diff --git a/internal/apps/php/init.go b/internal/apps/php/init.go index 73c9ab93..989fe38e 100644 --- a/internal/apps/php/init.go +++ b/internal/apps/php/init.go @@ -1,6 +1,8 @@ package php import ( + "fmt" + "github.com/go-chi/chi/v5" "github.com/TheTNB/panel/pkg/apploader" @@ -8,76 +10,25 @@ import ( ) 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) - }, - }) + php := []uint{74, 80, 81, 82, 83} + for _, version := range php { + apploader.Register(&types.App{ + Slug: fmt.Sprintf("php%d", version), + Route: func(r chi.Router) { + service := NewService(version) + 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/service.go b/internal/apps/php/service.go index e8c1fec1..f6a34a93 100644 --- a/internal/apps/php/service.go +++ b/internal/apps/php/service.go @@ -138,17 +138,6 @@ func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) { 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, Extension{ - Name: "ionCube", - Slug: "ionCube Loader", - Description: "ionCube 是一个专业级的 PHP 加密解密工具。", - Installed: false, - }) - } - raw, err := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, s.version) if err != nil { service.Error(w, http.StatusInternalServerError, "%v", err) @@ -182,10 +171,10 @@ func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) { return } - cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug) + cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'install' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(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(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "install" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug) + cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'install' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug) } task := new(biz.Task) @@ -213,10 +202,10 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) { return } - cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug) + cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'uninstall' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(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(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "uninstall" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug) + cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'uninstall' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug) } task := new(biz.Task) @@ -237,137 +226,136 @@ func (s *Service) getExtensions() []Extension { { Name: "fileinfo", Slug: "fileinfo", - Description: "Fileinfo 是一个用于识别文件类型的库。", + Description: "Fileinfo 是一个用于识别文件类型的库", }, { Name: "OPcache", Slug: "Zend OPcache", - Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。", + Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销", }, { Name: "PhpRedis", Slug: "redis", - Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。", + Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据", }, { Name: "ImageMagick", Slug: "imagick", - Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。", + Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件", }, { Name: "exif", Slug: "exif", - Description: "通过 exif 扩展,您可以操作图像元数据。", + Description: "通过 exif 扩展,您可以操作图像元数据", }, { Name: "pdo_pgsql", Slug: "pdo_pgsql", - Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。", + Description: "pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问(需先安装PostgreSQL)", }, { Name: "imap", Slug: "imap", - Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。", + Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件", }, { Name: "zip", Slug: "zip", - Description: "Zip 是一个用于处理 ZIP 文件的库。", + Description: "Zip 是一个用于处理 ZIP 文件的库", }, { Name: "bz2", Slug: "bz2", - Description: "Bzip2 是一个用于压缩和解压缩文件的库。", + Description: "Bzip2 是一个用于压缩和解压缩文件的库", }, { Name: "readline", Slug: "readline", - Description: "Readline 是一个库,它提供了一种用于处理文本的接口。", + Description: "Readline 是一个库,它提供了一种用于处理文本的接口", }, { Name: "snmp", Slug: "snmp", - Description: "SNMP 是一种用于网络管理的协议。", + Description: "SNMP 是一种用于网络管理的协议", }, { Name: "ldap", Slug: "ldap", - Description: "LDAP 是一种用于访问目录服务的协议。", + Description: "LDAP 是一种用于访问目录服务的协议", }, { Name: "enchant", Slug: "enchant", - Description: "Enchant 是一个拼写检查库。", + Description: "Enchant 是一个拼写检查库", }, { Name: "pspell", Slug: "pspell", - Description: "Pspell 是一个拼写检查库。", + Description: "Pspell 是一个拼写检查库", }, { Name: "calendar", Slug: "calendar", - Description: "Calendar 是一个用于处理日期的库。", + Description: "Calendar 是一个用于处理日期的库", }, { Name: "gmp", Slug: "gmp", - Description: "GMP 是一个用于处理大整数的库。", + Description: "GMP 是一个用于处理大整数的库", }, { Name: "sysvmsg", Slug: "sysvmsg", - Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。", + Description: "Sysvmsg 是一个用于处理 System V 消息队列的库", }, { Name: "sysvsem", Slug: "sysvsem", - Description: "Sysvsem 是一个用于处理 System V 信号量的库。", + Description: "Sysvsem 是一个用于处理 System V 信号量的库", }, { Name: "sysvshm", Slug: "sysvshm", - Description: "Sysvshm 是一个用于处理 System V 共享内存的库。", + Description: "Sysvshm 是一个用于处理 System V 共享内存的库", }, { Name: "xsl", Slug: "xsl", - Description: "XSL 是一个用于处理 XML 文档的库。", + Description: "XSL 是一个用于处理 XML 文档的库", }, { Name: "intl", Slug: "intl", - Description: "Intl 是一个用于处理国际化和本地化的库。", + Description: "Intl 是一个用于处理国际化和本地化的库", }, { Name: "gettext", Slug: "gettext", - Description: "Gettext 是一个用于处理多语言的库。", + Description: "Gettext 是一个用于处理多语言的库", }, { Name: "igbinary", Slug: "igbinary", - Description: "Igbinary 是一个用于序列化和反序列化数据的库。", + Description: "Igbinary 是一个用于序列化和反序列化数据的库", + }, + { + Name: "ionCube", + Slug: "ionCube Loader", + Description: "ionCube 是一个专业级的 PHP 加密解密工具(需在 OPcache 之后安装)", }, { Name: "Swoole", Slug: "swoole", - Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", - }, - { - Name: "Swow", - Slug: "Swow", - Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", + Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展", }, } - // ionCube 只支持 PHP 8.3 以下版本 - if cast.ToUint(s.version) < 83 { + // Swow 不支持 PHP 8.0 以下版本 + if cast.ToUint(s.version) >= 80 { extensions = append(extensions, Extension{ - Name: "ionCube", - Slug: "ionCube Loader", - Description: "ionCube 是一个专业级的 PHP 加密解密工具。", - Installed: false, + Name: "Swow", + Slug: "Swow", + Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", }) } diff --git a/internal/data/container_image.go b/internal/data/container_image.go index 51f92521..9254ffa3 100644 --- a/internal/data/container_image.go +++ b/internal/data/container_image.go @@ -76,7 +76,7 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { if req.Auth { sb.WriteString(fmt.Sprintf("docker login -u %s -p %s", req.Username, req.Password)) - if _, err := shell.ExecfWithTimeout(1*time.Minute, sb.String()); err != nil { // nolint: govet + if _, err := shell.Exec(sb.String()); err != nil { return fmt.Errorf("login failed: %w", err) } sb.Reset() @@ -84,7 +84,7 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { sb.WriteString(fmt.Sprintf("docker pull %s", req.Name)) - if _, err := shell.Execf(sb.String()); err != nil { // nolint: govet + if _, err := shell.Exec(sb.String()); err != nil { return err } diff --git a/internal/data/container_network.go b/internal/data/container_network.go index 79fcb948..eabb303a 100644 --- a/internal/data/container_network.go +++ b/internal/data/container_network.go @@ -115,7 +115,7 @@ func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (stri sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) } - return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet + return shell.Exec(sb.String()) } // Remove 删除网络 diff --git a/internal/data/container_volume.go b/internal/data/container_volume.go index 15b84de2..7a82e1b0 100644 --- a/internal/data/container_volume.go +++ b/internal/data/container_volume.go @@ -89,7 +89,7 @@ func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) } - return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet + return shell.Exec(sb.String()) } // Remove 删除存储卷 diff --git a/internal/queuejob/process_task.go b/internal/queuejob/process_task.go index 05d04b53..cd78adfd 100644 --- a/internal/queuejob/process_task.go +++ b/internal/queuejob/process_task.go @@ -2,7 +2,9 @@ package queuejob import ( "errors" + "log/slog" + "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/pkg/shell" ) @@ -36,7 +38,7 @@ func (r *ProcessTask) Handle(args ...any) error { return err } - if _, err = shell.Execf(task.Shell); err != nil { // nolint: govet + if _, err = shell.Exec(task.Shell); err != nil { return err } @@ -48,5 +50,6 @@ func (r *ProcessTask) Handle(args ...any) error { } func (r *ProcessTask) ErrHandle(err error) { + app.Logger.Warn("background task failed", slog.Any("task_id", r.taskID), slog.Any("err", err)) _ = r.taskRepo.UpdateStatus(r.taskID, biz.TaskStatusFailed) } diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go index 99f97892..02ad5126 100644 --- a/internal/service/file_windows.go +++ b/internal/service/file_windows.go @@ -162,8 +162,26 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { Success(w, nil) } +func (s *FileService) Exist(w http.ResponseWriter, r *http.Request) { + binder := chix.NewBind(r) + defer binder.Release() + + var paths []string + if err := binder.Body(&paths); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + var results []bool + for item := range slices.Values(paths) { + results = append(results, io.Exists(item)) + } + + Success(w, results) +} + func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.FileMove](r) + req, err := Bind[request.FileControl](r) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return @@ -183,7 +201,7 @@ func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { } func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.FileCopy](r) + req, err := Bind[request.FileControl](r) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go index 8cf99bea..80ae1b82 100644 --- a/pkg/firewall/firewall.go +++ b/pkg/firewall/firewall.go @@ -266,7 +266,7 @@ func (r *Firewall) Forward(rule Forward, operation Operation) error { } ruleBuilder.WriteString(" --permanent") - _, err := shell.Execf(ruleBuilder.String()) // nolint: govet + _, err := shell.Exec(ruleBuilder.String()) if err != nil { return err } diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go index dad62c38..eff09e6d 100644 --- a/pkg/shell/exec.go +++ b/pkg/shell/exec.go @@ -16,7 +16,23 @@ import ( "github.com/creack/pty" ) -// Execf 执行 shell 命令 +// Exec 执行 shell 命令 +func Exec(shell string) (string, error) { + _ = os.Setenv("LC_ALL", "C") + cmd := exec.Command("bash", "-c", shell) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", shell, strings.TrimSpace(stderr.String())) + } + + return strings.TrimSpace(stdout.String()), nil +} + +// Execf 安全执行 shell 命令 func Execf(shell string, args ...any) (string, error) { if !preCheckArg(args) { return "", errors.New("command contains illegal characters") diff --git a/web/src/views/apps/php/PhpView.vue b/web/src/views/apps/php/PhpView.vue new file mode 100644 index 00000000..1eb39a97 --- /dev/null +++ b/web/src/views/apps/php/PhpView.vue @@ -0,0 +1,398 @@ + + + diff --git a/web/src/views/apps/php74/IndexView.vue b/web/src/views/apps/php74/IndexView.vue new file mode 100644 index 00000000..7581ae9b --- /dev/null +++ b/web/src/views/apps/php74/IndexView.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/views/apps/php74/route.ts b/web/src/views/apps/php74/route.ts new file mode 100644 index 00000000..f0b632bb --- /dev/null +++ b/web/src/views/apps/php74/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php74', + path: '/apps/php74', + component: Layout, + isHidden: true, + children: [ + { + name: 'apps-php74-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'PHP 7.4', + icon: 'logos:php', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/apps/php80/IndexView.vue b/web/src/views/apps/php80/IndexView.vue new file mode 100644 index 00000000..160b4c1b --- /dev/null +++ b/web/src/views/apps/php80/IndexView.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/views/apps/php80/route.ts b/web/src/views/apps/php80/route.ts new file mode 100644 index 00000000..e09cd44a --- /dev/null +++ b/web/src/views/apps/php80/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php80', + path: '/apps/php80', + component: Layout, + isHidden: true, + children: [ + { + name: 'apps-php80-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'PHP 8.0', + icon: 'logos:php', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/apps/php81/IndexView.vue b/web/src/views/apps/php81/IndexView.vue index e29b1771..9e1eb6ed 100644 --- a/web/src/views/apps/php81/IndexView.vue +++ b/web/src/views/apps/php81/IndexView.vue @@ -1,397 +1,11 @@ diff --git a/web/src/views/apps/php82/IndexView.vue b/web/src/views/apps/php82/IndexView.vue index 89965739..ef45ed77 100644 --- a/web/src/views/apps/php82/IndexView.vue +++ b/web/src/views/apps/php82/IndexView.vue @@ -1,397 +1,11 @@ diff --git a/web/src/views/apps/php83/IndexView.vue b/web/src/views/apps/php83/IndexView.vue index dea09c5f..278f21ac 100644 --- a/web/src/views/apps/php83/IndexView.vue +++ b/web/src/views/apps/php83/IndexView.vue @@ -1,397 +1,11 @@