diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 22ebf3b2..a8e8706c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -104,8 +104,8 @@ body: Please copy and paste any relevant log output. 可以把文件拖入这个区域以添加日志文件。 Files can be dragged into this area to add log files. - 面板日志文件可在 `/www/panel/storage/logs` 中找到。 - Panel log files can be found in `/www/panel/storage/logs`. + 面板日志文件可在安装目录 `panel/storage/logs` 中找到。 + Panel log files can be found in the installation directory `panel/storage/logs`. validations: required: false - type: textarea diff --git a/internal/apps/fail2ban/service.go b/internal/apps/fail2ban/service.go index 342d65d8..f8846865 100644 --- a/internal/apps/fail2ban/service.go +++ b/internal/apps/fail2ban/service.go @@ -11,6 +11,7 @@ import ( "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/panel" "github.com/TheTNB/panel/internal/service" "github.com/TheTNB/panel/pkg/io" "github.com/TheTNB/panel/pkg/os" @@ -127,7 +128,7 @@ maxretry = ` + jailMaxRetry + ` findtime = ` + jailFindTime + ` bantime = ` + jailBanTime + ` action = %(action_mwl)s -logpath = /www/wwwlogs/` + website.Name + `.log +logpath = ` + panel.Root + `/wwwlogs/` + website.Name + `.log # ` + jailWebsiteName + `-` + jailWebsiteMode + `-END ` raw += rule @@ -170,13 +171,13 @@ ignoreregex = filter = "sshd" port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") case "mysql": - logPath = "/www/server/mysql/mysql-error.log" + logPath = panel.Root + "/server/mysql/mysql-error.log" filter = "mysqld-auth" - port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'") + port, err = shell.Execf("cat %s/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'", panel.Root) case "pure-ftpd": logPath = "/var/log/messages" filter = "pure-ftpd" - port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`) + port, err = shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, panel.Root) default: service.Error(w, http.StatusUnprocessableEntity, "未知服务") return diff --git a/internal/apps/frp/init.go b/internal/apps/frp/init.go new file mode 100644 index 00000000..9ce0cef0 --- /dev/null +++ b/internal/apps/frp/init.go @@ -0,0 +1,19 @@ +package frp + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "frp", + Route: func(r chi.Router) { + service := NewService() + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + }, + }) +} diff --git a/internal/apps/frp/request.go b/internal/apps/frp/request.go new file mode 100644 index 00000000..28cec09e --- /dev/null +++ b/internal/apps/frp/request.go @@ -0,0 +1,10 @@ +package frp + +type Name struct { + Name string `form:"name" json:"name"` +} + +type UpdateConfig struct { + Name string `form:"name" json:"name"` + Config string `form:"config" json:"config"` +} diff --git a/internal/apps/frp/service.go b/internal/apps/frp/service.go new file mode 100644 index 00000000..bb436283 --- /dev/null +++ b/internal/apps/frp/service.go @@ -0,0 +1,73 @@ +package frp + +import ( + "fmt" + "net/http" + + "github.com/TheTNB/panel/internal/panel" + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type Service struct{} + +func NewService() *Service { + return &Service{} +} + +// GetConfig +// +// @Summary 获取配置 +// @Description 获取 Frp 配置 +// @Tags 插件-Frp +// @Produce json +// @Security BearerToken +// @Param service query string false "服务" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/frp/config [get] +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[Name](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + config, err := io.Read(fmt.Sprintf("%s/server/frp/%s.toml", panel.Root, req.Name)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, config) +} + +// UpdateConfig +// +// @Summary 更新配置 +// @Description 更新 Frp 配置 +// @Tags 插件-Frp +// @Produce json +// @Security BearerToken +// @Param data body requests.UpdateConfig true "request" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/frp/config [post] +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = io.Write(fmt.Sprintf("%s/server/frp/%s.toml", panel.Root, req.Name), req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = systemctl.Restart(req.Name); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} diff --git a/internal/apps/gitea/init.go b/internal/apps/gitea/init.go new file mode 100644 index 00000000..ab8fb10d --- /dev/null +++ b/internal/apps/gitea/init.go @@ -0,0 +1,19 @@ +package gitea + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "gitea", + Route: func(r chi.Router) { + service := NewService() + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + }, + }) +} diff --git a/internal/apps/gitea/request.go b/internal/apps/gitea/request.go new file mode 100644 index 00000000..201499a3 --- /dev/null +++ b/internal/apps/gitea/request.go @@ -0,0 +1,5 @@ +package gitea + +type UpdateConfig struct { + Config string `form:"config" json:"config"` +} diff --git a/internal/apps/gitea/service.go b/internal/apps/gitea/service.go new file mode 100644 index 00000000..e13c41a5 --- /dev/null +++ b/internal/apps/gitea/service.go @@ -0,0 +1,64 @@ +package gitea + +import ( + "fmt" + "net/http" + + "github.com/TheTNB/panel/internal/panel" + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type Service struct{} + +func NewService() *Service { + return &Service{} +} + +// GetConfig +// +// @Summary 获取配置 +// @Tags 插件-Gitea +// @Produce json +// @Security BearerToken +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/gitea/config [get] +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", panel.Root)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, config) +} + +// UpdateConfig +// +// @Summary 更新配置 +// @Tags 插件-Gitea +// @Produce json +// @Security BearerToken +// @Param data body requests.UpdateConfig true "request" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/gitea/config [post] +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = io.Write(fmt.Sprintf("%s/server/gitea/app.ini", panel.Root), req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = systemctl.Restart("gitea"); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} diff --git a/internal/apps/openresty/service.go b/internal/apps/openresty/service.go index 7e969916..b12b488c 100644 --- a/internal/apps/openresty/service.go +++ b/internal/apps/openresty/service.go @@ -9,6 +9,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/spf13/cast" + "github.com/TheTNB/panel/internal/panel" "github.com/TheTNB/panel/internal/service" "github.com/TheTNB/panel/pkg/io" "github.com/TheTNB/panel/pkg/shell" @@ -34,7 +35,7 @@ func NewService() *Service { // @Success 200 {object} h.SuccessResponse // @Router /plugins/openresty/config [get] func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { - config, err := io.Read("/www/server/openresty/conf/nginx.conf") + config, err := io.Read(fmt.Sprintf("%s/server/openresty/conf/nginx.conf", panel.Root)) if err != nil { service.Error(w, http.StatusInternalServerError, "获取配置失败") return @@ -58,7 +59,7 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) { service.Error(w, http.StatusInternalServerError, "配置不能为空") } - if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil { + if err := io.Write(fmt.Sprintf("%s/server/openresty/conf/nginx.conf", panel.Root), config, 0644); err != nil { service.Error(w, http.StatusInternalServerError, "保存配置失败") } @@ -79,11 +80,11 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} h.SuccessResponse // @Router /plugins/openresty/errorLog [get] func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) { - if !io.Exists("/www/wwwlogs/nginx_error.log") { + if !io.Exists(fmt.Sprintf("%s/wwwlogs/nginx_error.log", panel.Root)) { service.Success(w, "") } - out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log") + out, err := shell.Execf("tail -n 100 %s/%s", panel.Root, "/wwwlogs/openresty_error.log") if err != nil { service.Error(w, http.StatusInternalServerError, out) } @@ -100,7 +101,7 @@ func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} h.SuccessResponse // @Router /plugins/openresty/clearErrorLog [post] func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) { - if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil { + if out, err := shell.Execf("echo '' > %s/%s", panel.Root, "/wwwlogs/openresty_error.log"); err != nil { service.Error(w, http.StatusInternalServerError, out) } diff --git a/internal/apps/percona/init.go b/internal/apps/percona/init.go new file mode 100644 index 00000000..08947579 --- /dev/null +++ b/internal/apps/percona/init.go @@ -0,0 +1,26 @@ +package percona + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "percona", + Route: func(r chi.Router) { + service := NewService() + r.Get("load", service.Load) + r.Get("config", service.GetConfig) + r.Post("config", service.UpdateConfig) + r.Get("errorLog", service.ErrorLog) + r.Post("clearErrorLog", service.ClearErrorLog) + r.Get("slowLog", service.SlowLog) + r.Post("clearSlowLog", service.ClearSlowLog) + r.Get("rootPassword", service.GetRootPassword) + r.Post("rootPassword", service.SetRootPassword) + }, + }) +} diff --git a/internal/apps/percona/request.go b/internal/apps/percona/request.go new file mode 100644 index 00000000..c2138dd4 --- /dev/null +++ b/internal/apps/percona/request.go @@ -0,0 +1,9 @@ +package percona + +type UpdateConfig struct { + Config string `form:"config" json:"config"` +} + +type SetRootPassword struct { + Password string `form:"password" json:"password"` +} diff --git a/internal/apps/percona/service.go b/internal/apps/percona/service.go new file mode 100644 index 00000000..8631b55b --- /dev/null +++ b/internal/apps/percona/service.go @@ -0,0 +1,248 @@ +package percona + +import ( + "fmt" + "net/http" + "regexp" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/panel" + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/db" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" +) + +type Service struct { + settingRepo biz.SettingRepo +} + +func NewService() *Service { + return &Service{ + settingRepo: data.NewSettingRepo(), + } +} + +// GetConfig 获取配置 +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read(panel.Root + "/server/mysql/conf/my.cnf") + if err != nil { + service.Error(w, http.StatusInternalServerError, "获取 Percona 配置失败") + return + } + + service.Success(w, config) +} + +// UpdateConfig 保存配置 +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err := io.Write(panel.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, "写入 Percona 配置失败") + return + } + + if err := systemctl.Reload("mysqld"); err != nil { + service.Error(w, http.StatusInternalServerError, "重载 Percona 失败") + return + } + + service.Success(w, nil) +} + +// Load 获取负载 +func (s *Service) Load(w http.ResponseWriter, r *http.Request) { + rootPassword, err := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword) + if err != nil { + service.Error(w, http.StatusInternalServerError, "获取 Percona root密码失败") + return + + } + if len(rootPassword) == 0 { + service.Error(w, http.StatusUnprocessableEntity, "Percona root密码为空") + return + } + + status, _ := systemctl.Status("mysqld") + if !status { + service.Success(w, []types.NV{}) + return + } + + raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1") + if err != nil { + service.Error(w, http.StatusInternalServerError, "获取MySQL负载失败") + return + } + + var load []map[string]string + expressions := []struct { + regex string + name string + }{ + {`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"}, + {`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"}, + {`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"}, + {`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"}, + {`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"}, + {`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"}, + {`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"}, + {`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"}, + {`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"}, + {`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"}, + {`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"}, + {`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"}, + {`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"}, + {`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"}, + {`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"}, + {`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"}, + {`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"}, + {`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"}, + } + + for _, expression := range expressions { + re := regexp.MustCompile(expression.regex) + matches := re.FindStringSubmatch(raw) + if len(matches) > 1 { + d := map[string]string{"name": expression.name, "value": matches[1]} + if expression.name == "发送" || expression.name == "接收" { + d["value"] = str.FormatBytes(cast.ToFloat64(matches[1])) + } + + load = append(load, d) + } + } + + // 索引命中率 + readRequests := cast.ToFloat64(load[9]["value"]) + reads := cast.ToFloat64(load[10]["value"]) + load[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100) + // Innodb 索引命中率 + bufferPoolReads := cast.ToFloat64(load[11]["value"]) + bufferPoolReadRequests := cast.ToFloat64(load[12]["value"]) + load[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100) + + service.Success(w, load) +} + +// ErrorLog 获取错误日志 +func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) { + log, err := shell.Execf("tail -n 100 %s/server/mysql/mysql-error.log", panel.Root) + if err != nil { + service.Error(w, http.StatusInternalServerError, log) + return + } + + service.Success(w, log) +} + +// ClearErrorLog 清空错误日志 +func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) { + if out, err := shell.Execf("echo '' > %s/server/mysql/mysql-error.log", panel.Root); err != nil { + service.Error(w, http.StatusInternalServerError, out) + return + } + + service.Success(w, nil) +} + +// SlowLog 获取慢查询日志 +func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) { + log, err := shell.Execf("tail -n 100 %s/server/mysql/mysql-slow.log", panel.Root) + if err != nil { + service.Error(w, http.StatusInternalServerError, log) + return + } + + service.Success(w, log) +} + +// ClearSlowLog 清空慢查询日志 +func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) { + if out, err := shell.Execf("echo '' > %s/server/mysql/mysql-slow.log", panel.Root); err != nil { + service.Error(w, http.StatusInternalServerError, out) + return + } + + service.Success(w, nil) +} + +// GetRootPassword 获取root密码 +func (s *Service) GetRootPassword(w http.ResponseWriter, r *http.Request) { + rootPassword, err := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword) + if err != nil { + service.Error(w, http.StatusInternalServerError, "获取 Percona root密码失败") + return + } + if len(rootPassword) == 0 { + service.Error(w, http.StatusUnprocessableEntity, "Percona root密码为空") + return + } + + service.Success(w, rootPassword) +} + +// SetRootPassword 设置root密码 +func (s *Service) SetRootPassword(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[SetRootPassword](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword) + mysql, err := db.NewMySQL("root", oldRootPassword, s.getSock(), "unix") + if err != nil { + // 尝试安全模式直接改密 + if err = db.MySQLResetRootPassword(req.Password); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + } else { + if err = mysql.UserPassword("root", req.Password); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + } + if err = s.settingRepo.Set(biz.SettingKeyPerconaRootPassword, req.Password); err != nil { + service.Error(w, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err)) + return + } + + service.Success(w, nil) +} + +func (s *Service) getSock() string { + if io.Exists("/tmp/mysql.sock") { + return "/tmp/mysql.sock" + } + if io.Exists(panel.Root + "/server/mysql/config/my.cnf") { + config, _ := io.Read(panel.Root + "/server/mysql/config/my.cnf") + re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) + matches := re.FindStringSubmatch(config) + if len(matches) > 2 { + return matches[2] + } + } + if io.Exists("/etc/my.cnf") { + config, _ := io.Read("/etc/my.cnf") + re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) + matches := re.FindStringSubmatch(config) + if len(matches) > 2 { + return matches[2] + } + } + + return "/tmp/mysql.sock" +} diff --git a/internal/apps/php/init.go b/internal/apps/php/init.go new file mode 100644 index 00000000..73c9ab93 --- /dev/null +++ b/internal/apps/php/init.go @@ -0,0 +1,83 @@ +package php + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "php80", + Route: func(r chi.Router) { + service := NewService(80) + r.Get("/load", service.Load) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + r.Get("/fpmConfig", service.GetFPMConfig) + r.Post("/fpmConfig", service.UpdateFPMConfig) + r.Get("/errorLog", service.ErrorLog) + r.Get("/slowLog", service.SlowLog) + r.Post("/clearErrorLog", service.ClearErrorLog) + r.Post("/clearSlowLog", service.ClearSlowLog) + r.Get("/extensions", service.ExtensionList) + r.Post("/extensions", service.InstallExtension) + r.Delete("/extensions", service.UninstallExtension) + }, + }) + apploader.Register(&types.App{ + Slug: "php81", + Route: func(r chi.Router) { + service := NewService(81) + r.Get("/load", service.Load) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + r.Get("/fpmConfig", service.GetFPMConfig) + r.Post("/fpmConfig", service.UpdateFPMConfig) + r.Get("/errorLog", service.ErrorLog) + r.Get("/slowLog", service.SlowLog) + r.Post("/clearErrorLog", service.ClearErrorLog) + r.Post("/clearSlowLog", service.ClearSlowLog) + r.Get("/extensions", service.ExtensionList) + r.Post("/extensions", service.InstallExtension) + r.Delete("/extensions", service.UninstallExtension) + }, + }) + apploader.Register(&types.App{ + Slug: "php82", + Route: func(r chi.Router) { + service := NewService(82) + r.Get("/load", service.Load) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + r.Get("/fpmConfig", service.GetFPMConfig) + r.Post("/fpmConfig", service.UpdateFPMConfig) + r.Get("/errorLog", service.ErrorLog) + r.Get("/slowLog", service.SlowLog) + r.Post("/clearErrorLog", service.ClearErrorLog) + r.Post("/clearSlowLog", service.ClearSlowLog) + r.Get("/extensions", service.ExtensionList) + r.Post("/extensions", service.InstallExtension) + r.Delete("/extensions", service.UninstallExtension) + }, + }) + apploader.Register(&types.App{ + Slug: "php83", + Route: func(r chi.Router) { + service := NewService(83) + r.Get("/load", service.Load) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + r.Get("/fpmConfig", service.GetFPMConfig) + r.Post("/fpmConfig", service.UpdateFPMConfig) + r.Get("/errorLog", service.ErrorLog) + r.Get("/slowLog", service.SlowLog) + r.Post("/clearErrorLog", service.ClearErrorLog) + r.Post("/clearSlowLog", service.ClearSlowLog) + r.Get("/extensions", service.ExtensionList) + r.Post("/extensions", service.InstallExtension) + r.Delete("/extensions", service.UninstallExtension) + }, + }) +} diff --git a/internal/apps/php/request.go b/internal/apps/php/request.go new file mode 100644 index 00000000..bf559a4b --- /dev/null +++ b/internal/apps/php/request.go @@ -0,0 +1,9 @@ +package php + +type UpdateConfig struct { + Config string `form:"config" json:"config"` +} + +type ExtensionSlug struct { + Slug string `form:"slug" json:"slug"` +} diff --git a/internal/apps/php/service.go b/internal/apps/php/service.go new file mode 100644 index 00000000..d69bdd30 --- /dev/null +++ b/internal/apps/php/service.go @@ -0,0 +1,513 @@ +package php + +import ( + "fmt" + "net/http" + "regexp" + "slices" + "strings" + "time" + + "github.com/go-resty/resty/v2" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/panel" + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/types" +) + +type Service struct { + version uint + taskRepo biz.TaskRepo +} + +func NewService(version uint) *Service { + return &Service{ + version: version, + taskRepo: data.NewTaskRepo(), + } +} + +// GetConfig +// +// @Summary 获取配置 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/config [get] +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php.ini", panel.Root, s.version)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, config) +} + +// UpdateConfig +// +// @Summary 保存配置 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Param config body string true "配置" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/config [post] +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php.ini", panel.Root, s.version), req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} + +// GetFPMConfig +// +// @Summary 获取 FPM 配置 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/fpmConfig [get] +func (s *Service) GetFPMConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", panel.Root, s.version)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, config) +} + +// UpdateFPMConfig +// +// @Summary 保存 FPM 配置 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Param config body string true "配置" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/fpmConfig [post] +func (s *Service) UpdateFPMConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", panel.Root, s.version), req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} + +// Load +// +// @Summary 获取负载状态 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/load [get] +func (s *Service) Load(w http.ResponseWriter, r *http.Request) { + client := resty.New().SetTimeout(10 * time.Second) + resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d", s.version)) + if err != nil || !resp.IsSuccess() { + service.Error(w, http.StatusInternalServerError, "获取负载状态失败") + return + } + + raw := resp.String() + dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"} + regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"} + + data := make([]types.NV, len(dataKeys)) + for i := range dataKeys { + data[i].Name = dataKeys[i] + + r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i])) + match := r.FindStringSubmatch(raw) + + if len(match) > 1 { + data[i].Value = strings.TrimSpace(match[1]) + } + } + + service.Success(w, data) +} + +// ErrorLog +// +// @Summary 获取错误日志 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/errorLog [get] +func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) { + log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/php-fpm.log", panel.Root, s.version) + service.Success(w, log) +} + +// SlowLog +// +// @Summary 获取慢日志 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/slowLog [get] +func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) { + log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/slow.log", panel.Root, s.version) + service.Success(w, log) +} + +// ClearErrorLog +// +// @Summary 清空错误日志 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/clearErrorLog [post] +func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) { + if out, err := shell.Execf("echo '' > %s/server/php/%d/var/log/php-fpm.log", panel.Root, s.version); err != nil { + service.Error(w, http.StatusInternalServerError, out) + return + } + + service.Success(w, nil) +} + +// ClearSlowLog +// +// @Summary 清空慢日志 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/clearSlowLog [post] +func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) { + if out, err := shell.Execf("echo '' > %s/server/php/%d/var/log/slow.log", panel.Root, s.version); err != nil { + service.Error(w, http.StatusInternalServerError, out) + return + } + + service.Success(w, nil) +} + +// ExtensionList +// +// @Summary 获取扩展列表 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/extensions [get] +func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) { + extensions := s.getExtensions() + + // ionCube 只支持 PHP 8.3 以下版本 + if cast.ToUint(s.version) < 83 { + extensions = append(extensions, types.PHPExtension{ + Name: "ionCube", + Slug: "ionCube Loader", + Description: "ionCube 是一个专业级的 PHP 加密解密工具。", + Installed: false, + }) + } + + raw, err := shell.Execf("%s/server/php/%d/bin/php -m", panel.Root, s.version) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + extensionMap := make(map[string]*types.PHPExtension) + for i := range extensions { + extensionMap[extensions[i].Slug] = &extensions[i] + } + + rawExtensionList := strings.Split(raw, "\n") + for _, item := range rawExtensionList { + if ext, exists := extensionMap[item]; exists && !strings.Contains(item, "[") && item != "" { + ext.Installed = true + } + } + + service.Success(w, extensions) +} + +// InstallExtension +// +// @Summary 安装扩展 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Param slug query string true "slug" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/extensions [post] +func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[ExtensionSlug](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if !s.checkExtension(req.Slug) { + service.Error(w, http.StatusUnprocessableEntity, "扩展不存在") + return + } + + cmd := fmt.Sprintf(`bash '%s/panel/scripts/php_extensions/%s.sh' install %d >> '/tmp/%s.log' 2>&1`, panel.Root, req.Slug, s.version, req.Slug) + officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} + if slices.Contains(officials, req.Slug) { + cmd = fmt.Sprintf(`bash '%s/panel/scripts/php_extensions/official.sh' install '%d' '%s' >> '/tmp/%s.log' 2>&1`, panel.Root, s.version, req.Slug, req.Slug) + } + + task := new(biz.Task) + task.Name = fmt.Sprintf("安装 PHP-%d 扩展 %s", s.version, req.Slug) + task.Status = biz.TaskStatusWaiting + task.Shell = cmd + task.Log = "/tmp/" + req.Slug + ".log" + if err = s.taskRepo.Push(task); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} + +// UninstallExtension +// +// @Summary 卸载扩展 +// @Tags 插件-PHP +// @Produce json +// @Security BearerToken +// @Param version path int true "PHP 版本" +// @Param slug query string true "slug" +// @Success 200 {object} controllers.SuccessResponse +// @Router /plugins/php/{version}/extensions [delete] +func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[ExtensionSlug](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if !s.checkExtension(req.Slug) { + service.Error(w, http.StatusUnprocessableEntity, "扩展不存在") + return + } + + cmd := fmt.Sprintf(`bash '%s/panel/scripts/php_extensions/%s.sh' uninstall %d >> '/tmp/%s.log' 2>&1`, panel.Root, req.Slug, s.version, req.Slug) + officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} + if slices.Contains(officials, req.Slug) { + cmd = fmt.Sprintf(`bash '%s/panel/scripts/php_extensions/official.sh' uninstall '%d' '%s' >> '/tmp/%s.log' 2>&1`, panel.Root, s.version, req.Slug, req.Slug) + } + + task := new(biz.Task) + task.Name = fmt.Sprintf("卸载 PHP-%d 扩展 %s", s.version, req.Slug) + task.Status = biz.TaskStatusWaiting + task.Shell = cmd + task.Log = "/tmp/" + req.Slug + ".log" + if err = s.taskRepo.Push(task); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, nil) +} + +func (s *Service) getExtensions() []types.PHPExtension { + extensions := []types.PHPExtension{ + { + Name: "fileinfo", + Slug: "fileinfo", + Description: "Fileinfo 是一个用于识别文件类型的库。", + }, + { + Name: "OPcache", + Slug: "Zend OPcache", + Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。", + }, + { + Name: "PhpRedis", + Slug: "redis", + Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。", + }, + { + Name: "ImageMagick", + Slug: "imagick", + Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。", + }, + { + Name: "exif", + Slug: "exif", + Description: "通过 exif 扩展,你可以操作图像元数据。", + }, + { + Name: "pdo_pgsql", + Slug: "pdo_pgsql", + Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。", + }, + { + Name: "imap", + Slug: "imap", + Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。", + }, + { + Name: "zip", + Slug: "zip", + Description: "Zip 是一个用于处理 ZIP 文件的库。", + }, + { + Name: "bz2", + Slug: "bz2", + Description: "Bzip2 是一个用于压缩和解压缩文件的库。", + }, + { + Name: "readline", + Slug: "readline", + Description: "Readline 是一个库,它提供了一种用于处理文本的接口。", + }, + { + Name: "snmp", + Slug: "snmp", + Description: "SNMP 是一种用于网络管理的协议。", + }, + { + Name: "ldap", + Slug: "ldap", + Description: "LDAP 是一种用于访问目录服务的协议。", + }, + { + Name: "enchant", + Slug: "enchant", + Description: "Enchant 是一个拼写检查库。", + }, + { + Name: "pspell", + Slug: "pspell", + Description: "Pspell 是一个拼写检查库。", + }, + { + Name: "calendar", + Slug: "calendar", + Description: "Calendar 是一个用于处理日期的库。", + }, + { + Name: "gmp", + Slug: "gmp", + Description: "GMP 是一个用于处理大整数的库。", + }, + { + Name: "sysvmsg", + Slug: "sysvmsg", + Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。", + }, + { + Name: "sysvsem", + Slug: "sysvsem", + Description: "Sysvsem 是一个用于处理 System V 信号量的库。", + }, + { + Name: "sysvshm", + Slug: "sysvshm", + Description: "Sysvshm 是一个用于处理 System V 共享内存的库。", + }, + { + Name: "xsl", + Slug: "xsl", + Description: "XSL 是一个用于处理 XML 文档的库。", + }, + { + Name: "intl", + Slug: "intl", + Description: "Intl 是一个用于处理国际化和本地化的库。", + }, + { + Name: "gettext", + Slug: "gettext", + Description: "Gettext 是一个用于处理多语言的库。", + }, + { + Name: "igbinary", + Slug: "igbinary", + Description: "Igbinary 是一个用于序列化和反序列化数据的库。", + }, + { + Name: "Swoole", + Slug: "swoole", + Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", + }, + { + Name: "Swow", + Slug: "Swow", + Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", + }, + } + + // ionCube 只支持 PHP 8.3 以下版本 + if cast.ToUint(s.version) < 83 { + extensions = append(extensions, types.PHPExtension{ + Name: "ionCube", + Slug: "ionCube Loader", + Description: "ionCube 是一个专业级的 PHP 加密解密工具。", + Installed: false, + }) + } + + raw, _ := shell.Execf("%s/server/php/%d/bin/php -m", panel.Root, s.version) + extensionMap := make(map[string]*types.PHPExtension) + for i := range extensions { + extensionMap[extensions[i].Slug] = &extensions[i] + } + + rawExtensionList := strings.Split(raw, "\n") + for _, item := range rawExtensionList { + if ext, exists := extensionMap[item]; exists && !strings.Contains(item, "[") && item != "" { + ext.Installed = true + } + } + + return extensions +} + +func (s *Service) checkExtension(slug string) bool { + extensions := s.getExtensions() + + for _, item := range extensions { + if item.Slug == slug { + return true + } + } + + return false +} diff --git a/internal/apps/phpmyadmin/init.go b/internal/apps/phpmyadmin/init.go new file mode 100644 index 00000000..22697a33 --- /dev/null +++ b/internal/apps/phpmyadmin/init.go @@ -0,0 +1,21 @@ +package phpmyadmin + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "phpmyadmin", + Route: func(r chi.Router) { + service := NewService() + r.Get("/info", service.Info) + r.Post("/port", service.UpdatePort) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + }, + }) +} diff --git a/internal/apps/phpmyadmin/request.go b/internal/apps/phpmyadmin/request.go new file mode 100644 index 00000000..52e5b5c6 --- /dev/null +++ b/internal/apps/phpmyadmin/request.go @@ -0,0 +1,9 @@ +package phpmyadmin + +type UpdateConfig struct { + Config string `form:"config" json:"config"` +} + +type UpdatePort struct { + Port uint `form:"port" json:"port"` +} diff --git a/internal/apps/phpmyadmin/service.go b/internal/apps/phpmyadmin/service.go new file mode 100644 index 00000000..d8a2d641 --- /dev/null +++ b/internal/apps/phpmyadmin/service.go @@ -0,0 +1,127 @@ +package phpmyadmin + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/go-rat/chix" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/panel" + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/firewall" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type Service struct{} + +func NewService() *Service { + return &Service{} +} + +func (s *Service) Info(w http.ResponseWriter, r *http.Request) { + files, err := io.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", panel.Root)) + if err != nil { + service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") + return + } + + var phpmyadmin string + for _, f := range files { + if strings.HasPrefix(f.Name(), "phpmyadmin_") { + phpmyadmin = f.Name() + } + } + if len(phpmyadmin) == 0 { + service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") + return + } + + conf, err := io.Read(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", panel.Root)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf) + if len(match) == 0 { + service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 端口") + return + } + + service.Success(w, chix.M{ + "path": phpmyadmin, + "port": cast.ToInt(match[1]), + }) +} + +func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdatePort](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + conf, err := io.Read(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", panel.Root)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(req.Port)+";") + if err = io.Write(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", panel.Root), conf, 0644); err != nil { + service.ErrorSystem(w) + return + } + + fw := firewall.NewFirewall() + err = fw.Port(firewall.FireInfo{ + Port: req.Port, + Protocol: "tcp", + }, firewall.OperationAdd) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + service.Error(w, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) + return + } + + service.Success(w, nil) +} + +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", panel.Root)) + if err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + service.Success(w, config) +} + +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = io.Write(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", panel.Root), req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + service.Error(w, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) + return + } + + service.Success(w, nil) +} diff --git a/internal/biz/setting.go b/internal/biz/setting.go index d09f3c3c..589dc784 100644 --- a/internal/biz/setting.go +++ b/internal/biz/setting.go @@ -9,17 +9,17 @@ import ( type SettingKey string const ( - SettingKeyName SettingKey = "name" - SettingKeyVersion SettingKey = "version" - SettingKeyMonitor SettingKey = "monitor" - SettingKeyMonitorDays SettingKey = "monitor_days" - SettingKeyBackupPath SettingKey = "backup_path" - SettingKeyWebsitePath SettingKey = "website_path" - SettingKeyMysqlRootPassword SettingKey = "mysql_root_password" - SettingKeySshHost SettingKey = "ssh_host" - SettingKeySshPort SettingKey = "ssh_port" - SettingKeySshUser SettingKey = "ssh_user" - SettingKeySshPassword SettingKey = "ssh_password" + SettingKeyName SettingKey = "name" + SettingKeyVersion SettingKey = "version" + SettingKeyMonitor SettingKey = "monitor" + SettingKeyMonitorDays SettingKey = "monitor_days" + SettingKeyBackupPath SettingKey = "backup_path" + SettingKeyWebsitePath SettingKey = "website_path" + SettingKeyPerconaRootPassword SettingKey = "percona_root_password" + SettingKeySshHost SettingKey = "ssh_host" + SettingKeySshPort SettingKey = "ssh_port" + SettingKeySshUser SettingKey = "ssh_user" + SettingKeySshPassword SettingKey = "ssh_password" ) type Setting struct { diff --git a/internal/biz/task.go b/internal/biz/task.go index 633c72e9..b76c0a72 100644 --- a/internal/biz/task.go +++ b/internal/biz/task.go @@ -27,4 +27,5 @@ type TaskRepo interface { Get(id uint) (*Task, error) Delete(id uint) error UpdateStatus(id uint, status TaskStatus) error + Push(task *Task) error } diff --git a/internal/data/app.go b/internal/data/app.go index a312b1ac..d0acaf3d 100644 --- a/internal/data/app.go +++ b/internal/data/app.go @@ -7,7 +7,6 @@ import ( "slices" "github.com/TheTNB/panel/internal/biz" - "github.com/TheTNB/panel/internal/job" "github.com/TheTNB/panel/internal/panel" "github.com/TheTNB/panel/pkg/api" "github.com/TheTNB/panel/pkg/apploader" @@ -145,13 +144,9 @@ func (r *appRepo) Install(slug string) error { task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, app.Slug) task.Log = "/tmp/" + app.Slug + ".log" - - if err = panel.Orm.Create(task).Error; err != nil { + if err = r.taskRepo.Push(task); err != nil { return err } - err = panel.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ - task.ID, - }) return err } @@ -189,13 +184,9 @@ func (r *appRepo) Uninstall(slug string) error { task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, app.Slug) task.Log = "/tmp/" + app.Slug + ".log" - - if err = panel.Orm.Create(task).Error; err != nil { + if err = r.taskRepo.Push(task); err != nil { return err } - err = panel.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ - task.ID, - }) return err } @@ -230,13 +221,9 @@ func (r *appRepo) Update(slug string) error { task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, app.Slug) task.Log = "/tmp/" + app.Slug + ".log" - - if err = panel.Orm.Create(task).Error; err != nil { + if err = r.taskRepo.Push(task); err != nil { return err } - err = panel.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ - task.ID, - }) return err } diff --git a/internal/data/cert.go b/internal/data/cert.go index d38b1959..32ac9687 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -171,7 +171,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { return nil, errors.New("通配符域名无法使用 HTTP 验证") } } - conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) + conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", panel.Root, cert.Website.Name) client.UseHTTP(conf, cert.Website.Path) } } diff --git a/internal/data/task.go b/internal/data/task.go index 516647a2..47aeeb50 100644 --- a/internal/data/task.go +++ b/internal/data/task.go @@ -2,6 +2,7 @@ package data import ( "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/job" "github.com/TheTNB/panel/internal/panel" ) @@ -37,3 +38,12 @@ func (r *taskRepo) Delete(id uint) error { func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error { return panel.Orm.Model(&biz.Task{}).Where("id = ?", id).Update("status", status).Error } + +func (r *taskRepo) Push(task *biz.Task) error { + if err := panel.Orm.Create(task).Error; err != nil { + return err + } + return panel.Queue.Push(job.NewProcessTask(r), []any{ + task.ID, + }) +} diff --git a/internal/data/website.go b/internal/data/website.go index 4e68f67d..29949dd3 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -332,7 +332,7 @@ server return nil, err } - rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + rootPassword, err := r.settingRepo.Get(biz.SettingKeyPerconaRootPassword) if err == nil && req.DB && req.DBType == "mysql" { mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") if err != nil { @@ -610,7 +610,7 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { _ = io.Remove(website.Path) } if req.DB { - rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + rootPassword, err := r.settingRepo.Get(biz.SettingKeyPerconaRootPassword) if err != nil { return err } diff --git a/internal/service/info.go b/internal/service/info.go index 3a5a9036..affca2eb 100644 --- a/internal/service/info.go +++ b/internal/service/info.go @@ -127,7 +127,7 @@ func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { } var databaseCount int64 if mysqlInstalled { - rootPassword, _ := s.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + rootPassword, _ := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword) mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock") if err == nil { defer mysql.Close() @@ -157,7 +157,7 @@ func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { } } if postgresqlInstalled { - postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432) + postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", panel.Root), 5432) if err == nil { defer postgres.Close() if err = postgres.Ping(); err != nil { diff --git a/pkg/db/postgres.go b/pkg/db/postgres.go index bf2a4915..d282deb9 100644 --- a/pkg/db/postgres.go +++ b/pkg/db/postgres.go @@ -17,10 +17,11 @@ type Postgres struct { username string password string address string + hbaFile string port uint } -func NewPostgres(username, password, address string, port uint) (*Postgres, error) { +func NewPostgres(username, password, address, hbaFile string, port uint) (*Postgres, error) { dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=disable", address, port, username, password) if password == "" { dsn = fmt.Sprintf("host=%s port=%d user=%s dbname=postgres sslmode=disable", address, port, username) @@ -37,6 +38,7 @@ func NewPostgres(username, password, address string, port uint) (*Postgres, erro username: username, password: password, address: address, + hbaFile: hbaFile, port: port, }, nil } @@ -90,7 +92,7 @@ func (m *Postgres) UserDrop(user string) error { return err } - _, _ = shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, user) + _, _ = shell.Execf(`sed -i '/%s/d' %s`, user, m.hbaFile) return systemctl.Reload("postgresql") } @@ -117,7 +119,7 @@ func (m *Postgres) PrivilegesRevoke(user, database string) error { func (m *Postgres) HostAdd(database, user, host string) error { config := fmt.Sprintf("host %s %s %s scram-sha-256", database, user, host) - if err := io.WriteAppend("/www/server/postgresql/data/pg_hba.conf", config); err != nil { + if err := io.WriteAppend(m.hbaFile, config); err != nil { return err } @@ -126,7 +128,7 @@ func (m *Postgres) HostAdd(database, user, host string) error { func (m *Postgres) HostRemove(database, user, host string) error { regex := fmt.Sprintf(`host\s+%s\s+%s\s+%s`, database, user, host) - if _, err := shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, regex); err != nil { + if _, err := shell.Execf(`sed -i '/%s/d' %s`, regex, m.hbaFile); err != nil { return err } diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go index 072079da..aed8f83f 100644 --- a/pkg/firewall/firewall.go +++ b/pkg/firewall/firewall.go @@ -13,6 +13,13 @@ import ( "github.com/TheTNB/panel/pkg/systemctl" ) +type Operation string + +var ( + OperationAdd Operation = "add" + OperationDel Operation = "remove" +) + type Firewall struct { forwardListRegex *regexp.Regexp richRuleRegex *regexp.Regexp @@ -125,7 +132,7 @@ func (r *Firewall) ListRichRule() ([]FireInfo, error) { return data, nil } -func (r *Firewall) Port(port FireInfo, operation string) error { +func (r *Firewall) Port(port FireInfo, operation Operation) error { stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d/%s --permanent", operation, port.Port, port.Protocol) if err != nil { return fmt.Errorf("%s port %d/%s failed, err: %s", operation, port.Port, port.Protocol, stdout) @@ -135,7 +142,7 @@ func (r *Firewall) Port(port FireInfo, operation string) error { return err } -func (r *Firewall) RichRules(rule FireInfo, operation string) error { +func (r *Firewall) RichRules(rule FireInfo, operation Operation) error { families := strings.Split(rule.Family, "/") // ipv4 ipv6 for _, family := range families { @@ -162,7 +169,7 @@ func (r *Firewall) RichRules(rule FireInfo, operation string) error { return err } -func (r *Firewall) PortForward(info Forward, operation string) error { +func (r *Firewall) PortForward(info Forward, operation Operation) error { if err := r.enableForward(); err != nil { return err }