mirror of
https://github.com/acepanel/panel.git
synced 2026-02-05 17:07:18 +08:00
feat: system helpers and website service
This commit is contained in:
@@ -43,7 +43,7 @@ TODO
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forum) 提问寻求帮助。
|
||||
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问寻求帮助。
|
||||
|
||||
对于面板自身问题,可在 GitHub 的`Issues`
|
||||
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
// Package services 网站服务
|
||||
package services
|
||||
|
||||
import "panel/app/models"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/packages/helpers"
|
||||
)
|
||||
|
||||
type Website interface {
|
||||
List() ([]models.Website, error)
|
||||
@@ -9,10 +19,11 @@ type Website interface {
|
||||
|
||||
type PanelWebsite struct {
|
||||
Name string `json:"name"`
|
||||
Status bool `json:"status"`
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
Php string `json:"php"`
|
||||
Ssl string `json:"ssl"`
|
||||
Php int `json:"php"`
|
||||
Ssl bool `json:"ssl"`
|
||||
Remark string `json:"remark"`
|
||||
Db bool `json:"db"`
|
||||
DbType string `json:"db_type"`
|
||||
@@ -43,3 +54,196 @@ type WebsiteSetting struct {
|
||||
Raw string `json:"raw"`
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
type WebsiteImpl struct {
|
||||
}
|
||||
|
||||
func NewWebsiteImpl() *WebsiteImpl {
|
||||
return &WebsiteImpl{}
|
||||
}
|
||||
|
||||
// List 列出网站
|
||||
func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) {
|
||||
var websites []models.Website
|
||||
var total int64
|
||||
if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil {
|
||||
return total, websites, err
|
||||
}
|
||||
|
||||
return total, websites, nil
|
||||
}
|
||||
|
||||
// Add 添加网站
|
||||
func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
// 禁止部分保留名称
|
||||
nameSlices := []string{"phpmyadmin", "mysql", "panel", "ssh"}
|
||||
if slices.Contains(nameSlices, website.Name) {
|
||||
return models.Website{}, errors.New("网站名称" + website.Name + "为保留名称,请更换")
|
||||
}
|
||||
|
||||
// path为空时,设置默认值
|
||||
if len(website.Path) == 0 {
|
||||
website.Path = "/www/wwwroot/" + website.Name
|
||||
}
|
||||
// path不为/开头时,返回错误
|
||||
if website.Path[0] != '/' {
|
||||
return models.Website{}, errors.New("网站路径" + website.Path + "必须以/开头")
|
||||
}
|
||||
|
||||
website.Ssl = false
|
||||
website.Status = true
|
||||
website.Domain = strings.TrimSpace(website.Domain)
|
||||
|
||||
w := models.Website{
|
||||
Name: website.Name,
|
||||
Status: website.Status,
|
||||
Path: website.Path,
|
||||
Php: website.Php,
|
||||
Ssl: website.Ssl,
|
||||
Remark: website.Remark,
|
||||
}
|
||||
if err := facades.Orm().Query().Create(&w); err != nil {
|
||||
return w, err
|
||||
}
|
||||
|
||||
helpers.Mkdir(website.Path, 0755)
|
||||
|
||||
index := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>耗子Linux面板</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>耗子Linux面板</h1>
|
||||
<p>这是耗子Linux面板的网站默认页面!</p>
|
||||
<p>当您看到此页面,说明您的网站已创建成功。</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`
|
||||
helpers.WriteFile(website.Path+"/index.html", index, 0644)
|
||||
|
||||
domainArr := strings.Split(website.Domain, "\n")
|
||||
portList := ""
|
||||
portArr := make(map[string]bool)
|
||||
domainList := ""
|
||||
for key, value := range domainArr {
|
||||
temp := strings.Split(value, ":")
|
||||
domainList += " " + temp[0]
|
||||
|
||||
if len(temp) < 2 {
|
||||
if _, ok := portArr["80"]; !ok {
|
||||
if key == len(domainArr)-1 {
|
||||
portList += " listen 80;"
|
||||
} else {
|
||||
portList += " listen 80;\n"
|
||||
}
|
||||
portArr["80"] = true
|
||||
}
|
||||
} else {
|
||||
if _, ok := portArr[temp[1]]; !ok {
|
||||
if key == len(domainArr)-1 {
|
||||
portList += " listen " + temp[1] + ";"
|
||||
} else {
|
||||
portList += " listen " + temp[1] + ";\n"
|
||||
}
|
||||
portArr[temp[1]] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nginxConf := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
%s
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name%s;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
# waf标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
#error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include /www/server/vhost/openresty/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
helpers.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", nginxConf, 0644)
|
||||
helpers.WriteFile("/www/server/panel/vhost/openresty/rewrite/"+website.Name+".conf", "", 0644)
|
||||
helpers.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".pem", "", 0644)
|
||||
helpers.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".key", "", 0644)
|
||||
|
||||
helpers.ExecShellAsync("systemctl reload openresty")
|
||||
|
||||
// TODO 创建数据库
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Delete 删除网站
|
||||
func (r *WebsiteImpl) Delete(name string) error {
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).First(&website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := facades.Orm().Query().Delete(&website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helpers.RemoveFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
|
||||
helpers.RemoveFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf")
|
||||
helpers.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".pem")
|
||||
helpers.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".key")
|
||||
helpers.RemoveFile(website.Path)
|
||||
|
||||
helpers.ExecShellAsync("systemctl reload openresty")
|
||||
|
||||
// TODO 删除数据库
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,17 @@ func FormatBytes(size float64) string {
|
||||
|
||||
// Cut 裁剪字符串
|
||||
func Cut(begin, end, str string) string {
|
||||
b := utf8.RuneCountInString(str[:strings.Index(str, begin)]) + utf8.RuneCountInString(begin)
|
||||
e := utf8.RuneCountInString(str[:strings.Index(str, end)]) - b
|
||||
return string([]rune(str)[b : b+e])
|
||||
bIndex := strings.Index(str, begin)
|
||||
eIndex := strings.Index(str, end)
|
||||
if bIndex == -1 || eIndex == -1 || bIndex > eIndex {
|
||||
return ""
|
||||
}
|
||||
|
||||
b := utf8.RuneCountInString(str[:bIndex]) + utf8.RuneCountInString(begin)
|
||||
e := utf8.RuneCountInString(str[:eIndex])
|
||||
if b > e {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string([]rune(str)[b:e])
|
||||
}
|
||||
|
||||
109
packages/helpers/system.go
Normal file
109
packages/helpers/system.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
// WriteFile 写入文件
|
||||
func WriteFile(path string, data string, permission os.FileMode) bool {
|
||||
if err := os.MkdirAll(filepath.Dir(path), permission); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 创建目录失败: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
err := os.WriteFile(path, []byte(data), permission)
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 写入文件 %s 失败: %s", path, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadFile 读取文件
|
||||
func ReadFile(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 读取文件 %s 失败: %s", path, err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// RemoveFile 删除文件
|
||||
func RemoveFile(path string) bool {
|
||||
if err := os.Remove(path); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 删除文件 %s 失败: %s", path, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ExecShell 执行 shell 命令
|
||||
func ExecShell(shell string) string {
|
||||
cmd := exec.Command("bash", "-c", shell)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// ExecShellAsync 异步执行 shell 命令
|
||||
func ExecShellAsync(shell string) {
|
||||
cmd := exec.Command("bash", "-c", shell)
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Mkdir 创建目录
|
||||
func Mkdir(path string, permission os.FileMode) bool {
|
||||
if err := os.MkdirAll(path, permission); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 创建目录 %s 失败: %s", path, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Chmod 修改文件权限
|
||||
func Chmod(path string, permission os.FileMode) bool {
|
||||
if err := os.Chmod(path, permission); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 修改文件 %s 权限失败: %s", path, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Chown 修改路径所有者
|
||||
func Chown(path, user, group string) bool {
|
||||
cmd := exec.Command("chown", "-R", user+":"+group, path)
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 修改路径 %s 所有者失败: %s", path, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
92
packages/helpers/system_test.go
Normal file
92
packages/helpers/system_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type SystemHelperTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestSystemHelperTestSuite(t *testing.T) {
|
||||
suite.Run(t, &SystemHelperTestSuite{})
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestWriteFile() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
s.True(WriteFile(filePath, "test data", 0644))
|
||||
s.FileExists(filePath)
|
||||
|
||||
content, _ := os.ReadFile(filePath)
|
||||
s.Equal("test data", string(content))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestReadFile() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.Equal("test data", ReadFile(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestRemoveFile() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(RemoveFile(filePath))
|
||||
s.False(RemoveFile(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestExecShell() {
|
||||
s.Equal("test\n", ExecShell("echo 'test'"))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestExecShellAsync() {
|
||||
command := "echo 'test' > /tmp/testfile"
|
||||
defer os.Remove("/tmp/testfile")
|
||||
|
||||
ExecShellAsync(command)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
content, _ := os.ReadFile("/tmp/testfile")
|
||||
s.Equal("test\n", string(content))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestMkdir() {
|
||||
dirPath := "/tmp/testdir"
|
||||
defer os.RemoveAll(dirPath)
|
||||
|
||||
s.True(Mkdir(dirPath, 0755))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestChmod() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(Chmod(filePath, 0755))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestChown() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(Chown(filePath, "runner", "runner"))
|
||||
}
|
||||
Reference in New Issue
Block a user