2
0
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:
耗子
2023-07-11 01:26:04 +08:00
parent e2c2717852
commit b606431305
5 changed files with 422 additions and 7 deletions

View File

@@ -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)。

View File

@@ -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
}

View File

@@ -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
View 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
}

View 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"))
}