diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go
index a0d87557..c792c9f4 100644
--- a/cmd/ace/wire_gen.go
+++ b/cmd/ace/wire_gen.go
@@ -90,6 +90,9 @@ func initWeb() (*app.Web, error) {
certDNSService := service.NewCertDNSService(certDNSRepo)
certAccountService := service.NewCertAccountService(certAccountRepo)
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
+ environmentRepo := data.NewEnvironmentRepo(locale, config, cacheRepo, taskRepo)
+ environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
+ environmentPHPService := service.NewEnvironmentPHPService(locale, environmentRepo, taskRepo)
cronService := service.NewCronService(cronRepo)
processService := service.NewProcessService()
safeRepo := data.NewSafeRepo()
@@ -134,7 +137,7 @@ func initWeb() (*app.Web, error) {
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
- http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, loader)
+ http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, loader)
wsService := service.NewWsService(locale, config, logger, sshRepo)
ws := route.NewWs(wsService)
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
diff --git a/go.sum b/go.sum
index 2b43e86e..72e7e4b4 100644
--- a/go.sum
+++ b/go.sum
@@ -118,6 +118,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
+github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -267,6 +269,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -375,6 +378,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -447,6 +452,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
diff --git a/internal/apps/php/app.go b/internal/apps/php/app.go
deleted file mode 100644
index ababfa5f..00000000
--- a/internal/apps/php/app.go
+++ /dev/null
@@ -1,523 +0,0 @@
-package php
-
-import (
- "fmt"
- "net/http"
- "net/url"
- "slices"
- "strings"
- "time"
-
- "github.com/go-chi/chi/v5"
- "github.com/go-resty/resty/v2"
- "github.com/leonelquinteros/gotext"
- "github.com/spf13/cast"
-
- "github.com/acepanel/panel/internal/app"
- "github.com/acepanel/panel/internal/biz"
- "github.com/acepanel/panel/internal/service"
- "github.com/acepanel/panel/pkg/io"
- "github.com/acepanel/panel/pkg/shell"
- "github.com/acepanel/panel/pkg/types"
-)
-
-type App struct {
- version uint
- t *gotext.Locale
- taskRepo biz.TaskRepo
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- t: t,
- taskRepo: task,
- }
-}
-
-func (s *App) Route(version uint) func(r chi.Router) {
- return func(r chi.Router) {
- php := new(App)
- php.version = version
- php.t = s.t
- php.taskRepo = s.taskRepo
- r.Post("/set_cli", php.SetCli)
- r.Get("/config", php.GetConfig)
- r.Post("/config", php.UpdateConfig)
- r.Get("/fpm_config", php.GetFPMConfig)
- r.Post("/fpm_config", php.UpdateFPMConfig)
- r.Get("/load", php.Load)
- r.Get("/log", php.Log)
- r.Get("/slow_log", php.SlowLog)
- r.Post("/clear_log", php.ClearLog)
- r.Post("/clear_slow_log", php.ClearSlowLog)
- r.Get("/extensions", php.ExtensionList)
- r.Post("/extensions", php.InstallExtension)
- r.Delete("/extensions", php.UninstallExtension)
- }
-}
-
-func (s *App) SetCli(w http.ResponseWriter, r *http.Request) {
- if _, err := shell.Execf("ln -sf %s/server/php/%d/bin/php /usr/local/bin/php", app.Root, s.version); err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
- config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, s.version))
- if err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, config)
-}
-
-func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
- req, err := service.Bind[UpdateConfig](r)
- if err != nil {
- service.Error(w, http.StatusUnprocessableEntity, "%v", err)
- return
- }
-
- if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, s.version), req.Config, 0644); err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) GetFPMConfig(w http.ResponseWriter, r *http.Request) {
- config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, s.version))
- if err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, config)
-}
-
-func (s *App) UpdateFPMConfig(w http.ResponseWriter, r *http.Request) {
- req, err := service.Bind[UpdateConfig](r)
- if err != nil {
- service.Error(w, http.StatusUnprocessableEntity, "%v", err)
- return
- }
-
- if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, s.version), req.Config, 0644); err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) Load(w http.ResponseWriter, r *http.Request) {
- var raw map[string]any
- client := resty.New().SetTimeout(10 * time.Second)
- _, err := client.R().SetResult(&raw).Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d?json", s.version))
- if err != nil {
- service.Success(w, []types.NV{})
- return
- }
-
- dataKeys := []string{
- s.t.Get("Application Pool"),
- s.t.Get("Process Manager"),
- s.t.Get("Start Time"),
- s.t.Get("Accepted Connections"),
- s.t.Get("Listen Queue"),
- s.t.Get("Max Listen Queue"),
- s.t.Get("Listen Queue Length"),
- s.t.Get("Idle Processes"),
- s.t.Get("Active Processes"),
- s.t.Get("Total Processes"),
- s.t.Get("Max Active Processes"),
- s.t.Get("Max Children Reached"),
- s.t.Get("Slow Requests"),
- }
- rawKeys := []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",
- }
-
- loads := make([]types.NV, 0)
- for i := range dataKeys {
- v, ok := raw[rawKeys[i]]
- if ok {
- loads = append(loads, types.NV{
- Name: dataKeys[i],
- Value: cast.ToString(v),
- })
- }
- }
-
- service.Success(w, loads)
-}
-
-func (s *App) Log(w http.ResponseWriter, r *http.Request) {
- service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, s.version))
-}
-
-func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
- service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, s.version))
-}
-
-func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
- if _, err := shell.Execf("cat /dev/null > %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version); err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
- if _, err := shell.Execf("cat /dev/null > %s/server/php/%d/var/log/slow.log", app.Root, s.version); err != nil {
- service.Error(w, http.StatusInternalServerError, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) ExtensionList(w http.ResponseWriter, r *http.Request) {
- extensions := s.getExtensions()
- 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)
- return
- }
-
- extensionMap := make(map[string]*Extension)
- 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)
-}
-
-func (s *App) InstallExtension(w http.ResponseWriter, r *http.Request) {
- req, err := service.Bind[ExtensionSlug](r)
- if err != nil {
- service.Error(w, http.StatusUnprocessableEntity, "%v", err)
- return
- }
-
- if !s.checkExtension(req.Slug) {
- service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
- return
- }
-
- cmd := fmt.Sprintf(`curl -sSLm 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", "pgsql", "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 -sSLm 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)
- task.Name = s.t.Get("Install PHP-%d %s extension", 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, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) UninstallExtension(w http.ResponseWriter, r *http.Request) {
- req, err := service.Bind[ExtensionSlug](r)
- if err != nil {
- service.Error(w, http.StatusUnprocessableEntity, "%v", err)
- return
- }
-
- if !s.checkExtension(req.Slug) {
- service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
- return
- }
-
- cmd := fmt.Sprintf(`curl -sSLm 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", "pgsql", "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 -sSLm 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)
- task.Name = s.t.Get("Uninstall PHP-%d %s extension", 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, "%v", err)
- return
- }
-
- service.Success(w, nil)
-}
-
-func (s *App) getExtensions() []Extension {
- extensions := []Extension{
- {
- Name: "fileinfo",
- Slug: "fileinfo",
- Description: s.t.Get("Fileinfo is a library used to identify file types"),
- },
- {
- Name: "OPcache",
- Slug: "Zend OPcache",
- Description: s.t.Get("OPcache stores precompiled PHP script bytecode in shared memory to improve PHP performance"),
- },
- {
- Name: "igbinary",
- Slug: "igbinary",
- Description: s.t.Get("Igbinary is a library for serializing and deserializing data"),
- },
- {
- Name: "Redis",
- Slug: "redis",
- Description: s.t.Get("PhpRedis connects to and operates on data in Redis databases (requires the igbinary extension installed above)"),
- },
- {
- Name: "Memcached",
- Slug: "memcached",
- Description: s.t.Get("Memcached is a driver for connecting to Memcached servers"),
- },
- {
- Name: "ImageMagick",
- Slug: "imagick",
- Description: s.t.Get("ImageMagick is free software for creating, editing, and composing images"),
- },
- {
- Name: "exif",
- Slug: "exif",
- Description: s.t.Get("Exif is a library for reading and writing image metadata"),
- },
- {
- Name: "pgsql",
- Slug: "pgsql",
- Description: s.t.Get("pgsql is a driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
- },
- {
- Name: "pdo_pgsql",
- Slug: "pdo_pgsql",
- Description: s.t.Get("pdo_pgsql is a PDO driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
- },
- {
- Name: "sqlsrv",
- Slug: "sqlsrv",
- Description: s.t.Get("sqlsrv is a driver for connecting to SQL Server"),
- },
- {
- Name: "pdo_sqlsrv",
- Slug: "pdo_sqlsrv",
- Description: s.t.Get("pdo_sqlsrv is a PDO driver for connecting to SQL Server"),
- },
- {
- Name: "imap",
- Slug: "imap",
- Description: s.t.Get("IMAP extension allows PHP to read, search, delete, download, and manage emails"),
- },
- {
- Name: "zip",
- Slug: "zip",
- Description: s.t.Get("Zip is a library for handling ZIP files"),
- },
- {
- Name: "bz2",
- Slug: "bz2",
- Description: s.t.Get("Bzip2 is a library for compressing and decompressing files"),
- },
- {
- Name: "ssh2",
- Slug: "ssh2",
- Description: s.t.Get("SSH2 is a library for connecting to SSH servers"),
- },
- {
- Name: "event",
- Slug: "event",
- Description: s.t.Get("Event is a library for handling events"),
- },
- {
- Name: "readline",
- Slug: "readline",
- Description: s.t.Get("Readline is a library for processing text"),
- },
- {
- Name: "snmp",
- Slug: "snmp",
- Description: s.t.Get("SNMP is a protocol for network management"),
- },
- {
- Name: "ldap",
- Slug: "ldap",
- Description: s.t.Get("LDAP is a protocol for accessing directory services"),
- },
- {
- Name: "enchant",
- Slug: "enchant",
- Description: s.t.Get("Enchant is a spell-checking library"),
- },
- {
- Name: "pspell",
- Slug: "pspell",
- Description: s.t.Get("Pspell is a spell-checking library"),
- },
- {
- Name: "calendar",
- Slug: "calendar",
- Description: s.t.Get("Calendar is a library for handling dates"),
- },
- {
- Name: "gmp",
- Slug: "gmp",
- Description: s.t.Get("GMP is a library for handling large integers"),
- },
- {
- Name: "xlswriter",
- Slug: "xlswriter",
- Description: s.t.Get("XLSWriter is a high-performance library for reading and writing Excel files"),
- },
- {
- Name: "xsl",
- Slug: "xsl",
- Description: s.t.Get("XSL is a library for processing XML documents"),
- },
- {
- Name: "intl",
- Slug: "intl",
- Description: s.t.Get("Intl is a library for handling internationalization and localization"),
- },
- {
- Name: "gettext",
- Slug: "gettext",
- Description: s.t.Get("Gettext is a library for handling multilingual support"),
- },
- {
- Name: "grpc",
- Slug: "grpc",
- Description: s.t.Get("gRPC is a high-performance, open-source, and general-purpose RPC framework"),
- },
- {
- Name: "protobuf",
- Slug: "protobuf",
- Description: s.t.Get("protobuf is a library for serializing and deserializing data"),
- },
- {
- Name: "rdkafka",
- Slug: "rdkafka",
- Description: s.t.Get("rdkafka is a library for connecting to Apache Kafka"),
- },
- {
- Name: "xhprof",
- Slug: "xhprof",
- Description: s.t.Get("xhprof is a library for performance profiling"),
- },
- {
- Name: "xdebug",
- Slug: "xdebug",
- Description: s.t.Get("xdebug is a library for debugging and profiling PHP code"),
- },
- {
- Name: "yaml",
- Slug: "yaml",
- Description: s.t.Get("yaml is a library for handling YAML"),
- },
- {
- Name: "zstd",
- Slug: "zstd",
- Description: s.t.Get("zstd is a library for compressing and decompressing files"),
- },
- {
- Name: "sysvmsg",
- Slug: "sysvmsg",
- Description: s.t.Get("Sysvmsg is a library for handling System V message queues"),
- },
- {
- Name: "sysvsem",
- Slug: "sysvsem",
- Description: s.t.Get("Sysvsem is a library for handling System V semaphores"),
- },
- {
- Name: "sysvshm",
- Slug: "sysvshm",
- Description: s.t.Get("Sysvshm is a library for handling System V shared memory"),
- },
- {
- Name: "ionCube",
- Slug: "ionCube Loader",
- Description: s.t.Get("ionCube is a professional-grade PHP encryption and decryption tool (must be installed after OPcache)"),
- },
- {
- Name: "Swoole",
- Slug: "swoole",
- Description: s.t.Get("Swoole is a PHP extension for building high-performance asynchronous concurrent servers"),
- },
- }
-
- // Swow 不支持 PHP 8.0 以下版本且目前不支持 PHP 8.4
- if cast.ToUint(s.version) >= 80 && cast.ToUint(s.version) < 84 {
- extensions = append(extensions, Extension{
- Name: "Swow",
- Slug: "Swow",
- Description: s.t.Get("Swow is a PHP extension for building high-performance asynchronous concurrent servers"),
- })
- }
- // PHP 8.4 移除了 pspell 和 imap 并且不再建议使用
- if cast.ToUint(s.version) >= 84 {
- extensions = slices.DeleteFunc(extensions, func(extension Extension) bool {
- return extension.Slug == "pspell" || extension.Slug == "imap"
- })
- }
-
- raw, _ := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, s.version)
- extensionMap := make(map[string]*Extension)
- 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 *App) checkExtension(slug string) bool {
- extensions := s.getExtensions()
-
- for _, item := range extensions {
- if item.Slug == slug {
- return true
- }
- }
-
- return false
-}
diff --git a/internal/apps/php/request.go b/internal/apps/php/request.go
deleted file mode 100644
index 1391aeae..00000000
--- a/internal/apps/php/request.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package php
-
-type UpdateConfig struct {
- Config string `form:"config" json:"config" validate:"required"`
-}
-
-type ExtensionSlug struct {
- Slug string `form:"slug" json:"slug" validate:"required"`
-}
diff --git a/internal/apps/php74/app.go b/internal/apps/php74/app.go
deleted file mode 100644
index 154ec52f..00000000
--- a/internal/apps/php74/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php74
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(74)(r)
-}
diff --git a/internal/apps/php80/app.go b/internal/apps/php80/app.go
deleted file mode 100644
index 89afa5f3..00000000
--- a/internal/apps/php80/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php80
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(80)(r)
-}
diff --git a/internal/apps/php81/app.go b/internal/apps/php81/app.go
deleted file mode 100644
index 6f448e5e..00000000
--- a/internal/apps/php81/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php81
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(81)(r)
-}
diff --git a/internal/apps/php82/app.go b/internal/apps/php82/app.go
deleted file mode 100644
index af6ab6cd..00000000
--- a/internal/apps/php82/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php82
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(82)(r)
-}
diff --git a/internal/apps/php83/app.go b/internal/apps/php83/app.go
deleted file mode 100644
index 477f9954..00000000
--- a/internal/apps/php83/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php83
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(83)(r)
-}
diff --git a/internal/apps/php84/app.go b/internal/apps/php84/app.go
deleted file mode 100644
index e7665c86..00000000
--- a/internal/apps/php84/app.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package php84
-
-import (
- "github.com/go-chi/chi/v5"
- "github.com/leonelquinteros/gotext"
-
- "github.com/acepanel/panel/internal/apps/php"
- "github.com/acepanel/panel/internal/biz"
-)
-
-type App struct {
- php *php.App
-}
-
-func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
- return &App{
- php: php.NewApp(t, task),
- }
-}
-
-func (s *App) Route(r chi.Router) {
- s.php.Route(84)(r)
-}
diff --git a/internal/biz/cache.go b/internal/biz/cache.go
index 415fae90..c2adeec2 100644
--- a/internal/biz/cache.go
+++ b/internal/biz/cache.go
@@ -5,9 +5,10 @@ import "time"
type CacheKey string
const (
- CacheKeyCategories CacheKey = "categories"
- CacheKeyApps CacheKey = "apps"
- CacheKeyRewrites CacheKey = "rewrites"
+ CacheKeyCategories CacheKey = "categories"
+ CacheKeyApps CacheKey = "apps"
+ CacheKeyEnvironment CacheKey = "environment"
+ CacheKeyRewrites CacheKey = "rewrites"
)
type Cache struct {
@@ -22,5 +23,6 @@ type CacheRepo interface {
Set(key CacheKey, value string) error
UpdateCategories() error
UpdateApps() error
+ UpdateEnvironments() error
UpdateRewrites() error
}
diff --git a/internal/biz/environment.go b/internal/biz/environment.go
new file mode 100644
index 00000000..59de0d5b
--- /dev/null
+++ b/internal/biz/environment.go
@@ -0,0 +1,16 @@
+package biz
+
+import (
+ "github.com/acepanel/panel/pkg/api"
+ "github.com/acepanel/panel/pkg/types"
+)
+
+type EnvironmentRepo interface {
+ Types() []types.LV
+ All(typ ...string) api.Environments
+ IsInstalled(typ, slug string) bool
+ HasUpdate(typ, slug string) bool
+ Install(typ, slug string) error
+ Uninstall(typ, slug string) error
+ Update(typ, slug string) error
+}
diff --git a/internal/data/app.go b/internal/data/app.go
index 45f26617..1c73c565 100644
--- a/internal/data/app.go
+++ b/internal/data/app.go
@@ -155,7 +155,6 @@ func (r *appRepo) GetHomeShow() ([]map[string]string, error) {
"name": loaded.Name,
"description": loaded.Description,
"slug": loaded.Slug,
- "icon": loaded.Icon,
"version": item.Version,
})
}
diff --git a/internal/data/cache.go b/internal/data/cache.go
index a17bb272..21ae520a 100644
--- a/internal/data/cache.go
+++ b/internal/data/cache.go
@@ -83,6 +83,20 @@ func (r *cacheRepo) UpdateApps() error {
return r.Set(biz.CacheKeyApps, string(encoded))
}
+func (r *cacheRepo) UpdateEnvironments() error {
+ environments, err := r.api.Environments()
+ if err != nil {
+ return err
+ }
+
+ encoded, err := json.Marshal(environments)
+ if err != nil {
+ return err
+ }
+
+ return r.Set(biz.CacheKeyEnvironment, string(encoded))
+}
+
func (r *cacheRepo) UpdateRewrites() error {
rewrites, err := r.api.RewritesByType("nginx")
if err != nil {
diff --git a/internal/data/data.go b/internal/data/data.go
index 102dbacd..d3a28eed 100644
--- a/internal/data/data.go
+++ b/internal/data/data.go
@@ -19,6 +19,7 @@ var ProviderSet = wire.NewSet(
NewDatabaseRepo,
NewDatabaseServerRepo,
NewDatabaseUserRepo,
+ NewEnvironmentRepo,
NewMonitorRepo,
NewSafeRepo,
NewSettingRepo,
diff --git a/internal/data/environment.go b/internal/data/environment.go
new file mode 100644
index 00000000..8ac4c1b3
--- /dev/null
+++ b/internal/data/environment.go
@@ -0,0 +1,141 @@
+package data
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+
+ "github.com/leonelquinteros/gotext"
+
+ "github.com/acepanel/panel/internal/app"
+ "github.com/acepanel/panel/internal/biz"
+ "github.com/acepanel/panel/pkg/api"
+ "github.com/acepanel/panel/pkg/config"
+ "github.com/acepanel/panel/pkg/shell"
+ "github.com/acepanel/panel/pkg/types"
+)
+
+type environmentRepo struct {
+ t *gotext.Locale
+ conf *config.Config
+ cache biz.CacheRepo
+ task biz.TaskRepo
+}
+
+func NewEnvironmentRepo(t *gotext.Locale, conf *config.Config, cache biz.CacheRepo, task biz.TaskRepo) biz.EnvironmentRepo {
+ return &environmentRepo{
+ t: t,
+ conf: conf,
+ cache: cache,
+ task: task,
+ }
+}
+
+func (r *environmentRepo) Types() []types.LV {
+ return []types.LV{
+ {Label: "PHP", Value: "php"},
+ }
+}
+
+func (r *environmentRepo) All(typ ...string) api.Environments {
+ cached, err := r.cache.Get(biz.CacheKeyEnvironment)
+ if err != nil {
+ return nil
+ }
+ var environments api.Environments
+ if err = json.Unmarshal([]byte(cached), &environments); err != nil {
+ return nil
+ }
+
+ // 过滤
+ slices.DeleteFunc(environments, func(env *api.Environment) bool {
+ return len(typ) > 0 && typ[0] != "" && env.Type != typ[0]
+ })
+
+ return environments
+}
+
+func (r *environmentRepo) GetByTypeAndSlug(typ, slug string) *api.Environment {
+ all := r.All()
+ for _, env := range all {
+ if env.Type == typ && env.Slug == slug {
+ return env
+ }
+ }
+ return nil
+}
+
+func (r *environmentRepo) IsInstalled(typ, slug string) bool {
+ path := filepath.Join(app.Root, "server", typ, slug)
+ exist, _ := os.Stat(path)
+ return exist != nil && exist.IsDir()
+}
+
+func (r *environmentRepo) HasUpdate(typ, slug string) bool {
+ if !r.IsInstalled(typ, slug) {
+ return false
+ }
+
+ var basePath = filepath.Join(app.Root, "server", typ, slug)
+ env := r.GetByTypeAndSlug(typ, slug)
+ if env == nil {
+ return false
+ }
+ mainlineVersion := env.Version
+
+ switch typ {
+ case "php":
+ installedVersion, err := shell.Exec(filepath.Join(basePath, "bin", "php") + " -v | head -n 1 | awk '{print $2}'")
+ if err != nil {
+ return false
+ }
+ return installedVersion != mainlineVersion
+ default:
+ return false
+ }
+}
+
+func (r *environmentRepo) Install(typ, slug string) error {
+ return r.do(typ, slug, "install")
+}
+
+func (r *environmentRepo) Uninstall(typ, slug string) error {
+ return r.do(typ, slug, "uninstall")
+}
+
+func (r *environmentRepo) Update(typ, slug string) error {
+ return r.do(typ, slug, "update")
+}
+
+func (r *environmentRepo) do(typ, slug, action string) error {
+ env := r.GetByTypeAndSlug(typ, slug)
+ if env == nil {
+ return fmt.Errorf("environment not found: %s-%s", typ, slug)
+ }
+
+ shellUrl := fmt.Sprintf("https://%s/%s/%s.sh", r.conf.App.DownloadEndpoint, typ, action)
+
+ if app.IsCli {
+ return shell.ExecfWithOutput(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s"`, shellUrl, slug)
+ }
+
+ var name string
+ switch action {
+ case "install":
+ name = r.t.Get("Install environment %s", env.Name)
+ case "uninstall":
+ name = r.t.Get("Uninstall environment %s", env.Name)
+ case "update":
+ name = r.t.Get("Update environment %s", env.Name)
+ }
+
+ task := new(biz.Task)
+ task.Name = name
+ task.Status = biz.TaskStatusWaiting
+ task.Shell = fmt.Sprintf(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" >> /tmp/%s-%s.log 2>&1`, shellUrl, slug, typ, slug)
+ task.Log = fmt.Sprintf("/tmp/%s-%s.log", typ, slug)
+
+ return r.task.Push(task)
+}
diff --git a/internal/http/request/environment.go b/internal/http/request/environment.go
new file mode 100644
index 00000000..65ae9572
--- /dev/null
+++ b/internal/http/request/environment.go
@@ -0,0 +1,7 @@
+package request
+
+// EnvironmentAction 环境操作请求
+type EnvironmentAction struct {
+ Type string `json:"type"`
+ Slug string `json:"slug"`
+}
diff --git a/internal/http/request/environment_php.go b/internal/http/request/environment_php.go
new file mode 100644
index 00000000..cdd53401
--- /dev/null
+++ b/internal/http/request/environment_php.go
@@ -0,0 +1,15 @@
+package request
+
+type EnvironmentPHPVersion struct {
+ Version uint `json:"version"`
+}
+
+type EnvironmentPHPModule struct {
+ Version uint `json:"version"`
+ Slug string `form:"slug" json:"slug" validate:"required"`
+}
+
+type EnvironmentPHPUpdateConfig struct {
+ Version uint `json:"version"`
+ Config string `form:"config" json:"config" validate:"required"`
+}
diff --git a/internal/route/http.go b/internal/route/http.go
index 175a449c..137bc80d 100644
--- a/internal/route/http.go
+++ b/internal/route/http.go
@@ -30,6 +30,8 @@ type Http struct {
certDNS *service.CertDNSService
certAccount *service.CertAccountService
app *service.AppService
+ environment *service.EnvironmentService
+ environmentPHP *service.EnvironmentPHPService
cron *service.CronService
process *service.ProcessService
safe *service.SafeService
@@ -64,6 +66,8 @@ func NewHttp(
certDNS *service.CertDNSService,
certAccount *service.CertAccountService,
app *service.AppService,
+ environment *service.EnvironmentService,
+ environmentPHP *service.EnvironmentPHPService,
cron *service.CronService,
process *service.ProcessService,
safe *service.SafeService,
@@ -97,6 +101,8 @@ func NewHttp(
certDNS: certDNS,
certAccount: certAccount,
app: app,
+ environment: environment,
+ environmentPHP: environmentPHP,
cron: cron,
process: process,
safe: safe,
@@ -262,6 +268,30 @@ func (route *Http) Register(r *chi.Mux) {
r.Get("/update_cache", route.app.UpdateCache)
})
+ r.Route("/environment", func(r chi.Router) {
+ r.Get("/types", route.environment.Types)
+ r.Get("/list", route.environment.List)
+ r.Post("/install", route.environment.Install)
+ r.Get("/uninstall", route.environment.Uninstall)
+ r.Put("/update", route.environment.Update)
+ r.Get("/is_installed", route.environment.IsInstalled)
+ r.Route("/php", func(r chi.Router) {
+ r.Post("/{version}/set_cli", route.environmentPHP.SetCli)
+ r.Get("/{version}/config", route.environmentPHP.GetConfig)
+ r.Post("/{version}/config", route.environmentPHP.UpdateConfig)
+ r.Get("/{version}/fpm_config", route.environmentPHP.GetFPMConfig)
+ r.Post("/{version}/fpm_config", route.environmentPHP.UpdateFPMConfig)
+ r.Get("/{version}/load", route.environmentPHP.Load)
+ r.Get("/{version}/log", route.environmentPHP.Log)
+ r.Get("/{version}/slow_log", route.environmentPHP.SlowLog)
+ r.Post("/{version}/clear_log", route.environmentPHP.ClearLog)
+ r.Post("/{version}/clear_slow_log", route.environmentPHP.ClearSlowLog)
+ r.Get("/{version}/modules", route.environmentPHP.ModuleList)
+ r.Post("/{version}/modules", route.environmentPHP.InstallModule)
+ r.Delete("/{version}/modules", route.environmentPHP.UninstallModule)
+ })
+ })
+
r.Route("/cron", func(r chi.Router) {
r.Get("/", route.cron.List)
r.Post("/", route.cron.Create)
diff --git a/internal/service/app.go b/internal/service/app.go
index 913bd1cb..bcb391be 100644
--- a/internal/service/app.go
+++ b/internal/service/app.go
@@ -49,7 +49,7 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
installedAppMap[p.Slug] = p
}
- var apps []types.AppCenter
+ var apps []types.AppDetail
for _, item := range all {
installed, installedChannel, installedVersion, updateExist, show := false, "", "", false, false
if _, ok := installedAppMap[item.Slug]; ok {
@@ -63,8 +63,7 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
continue
}
- app := types.AppCenter{
- Icon: item.Icon,
+ app := types.AppDetail{
Name: item.Name,
Description: item.Description,
Categories: item.Categories,
@@ -201,6 +200,10 @@ func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
+ if err := s.cacheRepo.UpdateEnvironments(); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
Success(w, nil)
}
diff --git a/internal/service/cli.go b/internal/service/cli.go
index 0642f2f2..f3b104f2 100644
--- a/internal/service/cli.go
+++ b/internal/service/cli.go
@@ -903,6 +903,8 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
}
conf.App.Key = str.Random(32)
+ conf.App.APIEndpoint = "api.acepanel.net"
+ conf.App.DownloadEndpoint = "dl.acepanel.net"
conf.HTTP.Entrance = "/" + str.Random(6)
// 随机默认端口
diff --git a/internal/service/environment.go b/internal/service/environment.go
index 515b844f..6b0de53c 100644
--- a/internal/service/environment.go
+++ b/internal/service/environment.go
@@ -1,13 +1,107 @@
package service
-import "github.com/leonelquinteros/gotext"
+import (
+ "net/http"
+ "strings"
+
+ "github.com/leonelquinteros/gotext"
+
+ "github.com/acepanel/panel/internal/biz"
+ "github.com/acepanel/panel/internal/http/request"
+ "github.com/acepanel/panel/pkg/types"
+)
type EnvironmentService struct {
- t *gotext.Locale
+ t *gotext.Locale
+ environmentRepo biz.EnvironmentRepo
+ taskRepo biz.TaskRepo
}
-func NewEnvironmentService(t *gotext.Locale) *EnvironmentService {
+func NewEnvironmentService(t *gotext.Locale, environmentRepo biz.EnvironmentRepo, taskRepo biz.TaskRepo) *EnvironmentService {
return &EnvironmentService{
- t: t,
+ t: t,
+ environmentRepo: environmentRepo,
+ taskRepo: taskRepo,
}
}
+
+func (s *EnvironmentService) Types(w http.ResponseWriter, r *http.Request) {
+ Success(w, s.environmentRepo.Types())
+}
+
+func (s *EnvironmentService) List(w http.ResponseWriter, r *http.Request) {
+ typ := r.URL.Query().Get("type")
+ all := s.environmentRepo.All()
+ var environments []types.EnvironmentDetail
+ for _, item := range all {
+ if typ != "" && !strings.EqualFold(item.Type, typ) {
+ continue
+ }
+ environments = append(environments, types.EnvironmentDetail{
+ Type: item.Type,
+ Name: item.Name,
+ Description: item.Description,
+ Slug: item.Slug,
+ Installed: s.environmentRepo.IsInstalled(item.Type, item.Slug),
+ HasUpdate: s.environmentRepo.HasUpdate(item.Type, item.Slug),
+ })
+ }
+
+ Success(w, environments)
+}
+
+func (s *EnvironmentService) Install(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentAction](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.environmentRepo.Install(req.Type, req.Slug); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentService) Uninstall(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentAction](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.environmentRepo.Uninstall(req.Type, req.Slug); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentService) Update(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentAction](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.environmentRepo.Update(req.Type, req.Slug); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentService) IsInstalled(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentAction](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ installed := s.environmentRepo.IsInstalled(req.Type, req.Slug)
+ Success(w, installed)
+}
diff --git a/internal/service/environment_php.go b/internal/service/environment_php.go
new file mode 100644
index 00000000..b1aec310
--- /dev/null
+++ b/internal/service/environment_php.go
@@ -0,0 +1,613 @@
+package service
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/leonelquinteros/gotext"
+ "github.com/spf13/cast"
+
+ "github.com/acepanel/panel/internal/app"
+ "github.com/acepanel/panel/internal/biz"
+ "github.com/acepanel/panel/internal/http/request"
+ "github.com/acepanel/panel/pkg/io"
+ "github.com/acepanel/panel/pkg/shell"
+ "github.com/acepanel/panel/pkg/types"
+)
+
+type EnvironmentPHPService struct {
+ t *gotext.Locale
+ environmentRepo biz.EnvironmentRepo
+ taskRepo biz.TaskRepo
+}
+
+func NewEnvironmentPHPService(t *gotext.Locale, environmentRepo biz.EnvironmentRepo, taskRepo biz.TaskRepo) *EnvironmentPHPService {
+ return &EnvironmentPHPService{
+ t: t,
+ environmentRepo: environmentRepo,
+ taskRepo: taskRepo,
+ }
+}
+
+func (s *EnvironmentPHPService) SetCli(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if _, err := shell.Execf("ln -sf %s/server/php/%d/bin/php /usr/local/bin/php", app.Root, req.Version); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) GetConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version))
+ if err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, config)
+}
+
+func (s *EnvironmentPHPService) UpdateConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPUpdateConfig](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version), req.Config, 0644); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) GetFPMConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version))
+ if err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, config)
+}
+
+func (s *EnvironmentPHPService) UpdateFPMConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPUpdateConfig](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if err = io.Write(fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version), req.Config, 0644); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) Load(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ var raw map[string]any
+ client := resty.New().SetTimeout(10 * time.Second)
+ _, err = client.R().SetResult(&raw).Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d?json", req.Version))
+ if err != nil {
+ Success(w, []types.NV{})
+ return
+ }
+
+ dataKeys := []string{
+ s.t.Get("Application Pool"),
+ s.t.Get("Process Manager"),
+ s.t.Get("Start Time"),
+ s.t.Get("Accepted Connections"),
+ s.t.Get("Listen Queue"),
+ s.t.Get("Max Listen Queue"),
+ s.t.Get("Listen Queue Length"),
+ s.t.Get("Idle Processes"),
+ s.t.Get("Active Processes"),
+ s.t.Get("Total Processes"),
+ s.t.Get("Max Active Processes"),
+ s.t.Get("Max Children Reached"),
+ s.t.Get("Slow Requests"),
+ }
+ rawKeys := []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",
+ }
+
+ loads := make([]types.NV, 0)
+ for i := range dataKeys {
+ v, ok := raw[rawKeys[i]]
+ if ok {
+ loads = append(loads, types.NV{
+ Name: dataKeys[i],
+ Value: cast.ToString(v),
+ })
+ }
+ }
+
+ Success(w, loads)
+}
+
+func (s *EnvironmentPHPService) Log(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, req.Version))
+}
+
+func (s *EnvironmentPHPService) SlowLog(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, req.Version))
+}
+
+func (s *EnvironmentPHPService) ClearLog(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if _, err = shell.Execf("cat /dev/null > %s/server/php/%d/var/log/php-fpm.log", app.Root, req.Version); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if _, err = shell.Execf("cat /dev/null > %s/server/php/%d/var/log/slow.log", app.Root, req.Version); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) ModuleList(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPVersion](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ modules := s.getModules(req.Version)
+ raw, err := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, req.Version)
+ if err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ moduleMap := make(map[string]*types.EnvironmentPHPModule)
+ for i := range modules {
+ moduleMap[modules[i].Slug] = &modules[i]
+ }
+
+ rawModuleList := strings.Split(raw, "\n")
+ for _, item := range rawModuleList {
+ if ext, exists := moduleMap[item]; exists && !strings.Contains(item, "[") && item != "" {
+ ext.Installed = true
+ }
+ }
+
+ Success(w, modules)
+}
+
+func (s *EnvironmentPHPService) InstallModule(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPModule](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if !s.checkModule(req.Version, req.Slug) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Slug))
+ return
+ }
+
+ cmd := fmt.Sprintf(`curl -sSLm 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), req.Version, req.Slug)
+ officials := []string{"fileinfo", "exif", "imap", "pgsql", "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 -sSLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'install' '%d' '%s' >> '/tmp/%s.log' 2>&1`, req.Version, req.Slug, req.Slug)
+ }
+
+ task := new(biz.Task)
+ task.Name = s.t.Get("Install PHP-%d %s module", req.Version, req.Slug)
+ task.Status = biz.TaskStatusWaiting
+ task.Shell = cmd
+ task.Log = "/tmp/" + req.Slug + ".log"
+ if err = s.taskRepo.Push(task); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) UninstallModule(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.EnvironmentPHPModule](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+ if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
+ return
+ }
+
+ if !s.checkModule(req.Version, req.Slug) {
+ Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Slug))
+ return
+ }
+
+ cmd := fmt.Sprintf(`curl -sSLm 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), req.Version, req.Slug)
+ officials := []string{"fileinfo", "exif", "imap", "pgsql", "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 -sSLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'uninstall' '%d' '%s' >> '/tmp/%s.log' 2>&1`, req.Version, req.Slug, req.Slug)
+ }
+
+ task := new(biz.Task)
+ task.Name = s.t.Get("Uninstall PHP-%d %s module", req.Version, req.Slug)
+ task.Status = biz.TaskStatusWaiting
+ task.Shell = cmd
+ task.Log = "/tmp/" + req.Slug + ".log"
+ if err = s.taskRepo.Push(task); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *EnvironmentPHPService) getModules(version uint) []types.EnvironmentPHPModule {
+ modules := []types.EnvironmentPHPModule{
+ {
+ Name: "fileinfo",
+ Slug: "fileinfo",
+ Description: s.t.Get("Fileinfo is a library used to identify file types"),
+ },
+ {
+ Name: "OPcache",
+ Slug: "Zend OPcache",
+ Description: s.t.Get("OPcache stores precompiled PHP script bytecode in shared memory to improve PHP performance"),
+ },
+ {
+ Name: "igbinary",
+ Slug: "igbinary",
+ Description: s.t.Get("Igbinary is a library for serializing and deserializing data"),
+ },
+ {
+ Name: "Redis",
+ Slug: "redis",
+ Description: s.t.Get("PhpRedis connects to and operates on data in Redis databases (requires the igbinary module installed above)"),
+ },
+ {
+ Name: "Memcached",
+ Slug: "memcached",
+ Description: s.t.Get("Memcached is a driver for connecting to Memcached servers"),
+ },
+ {
+ Name: "ImageMagick",
+ Slug: "imagick",
+ Description: s.t.Get("ImageMagick is free software for creating, editing, and composing images"),
+ },
+ {
+ Name: "exif",
+ Slug: "exif",
+ Description: s.t.Get("Exif is a library for reading and writing image metadata"),
+ },
+ {
+ Name: "pgsql",
+ Slug: "pgsql",
+ Description: s.t.Get("pgsql is a driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
+ },
+ {
+ Name: "pdo_pgsql",
+ Slug: "pdo_pgsql",
+ Description: s.t.Get("pdo_pgsql is a PDO driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
+ },
+ {
+ Name: "sqlsrv",
+ Slug: "sqlsrv",
+ Description: s.t.Get("sqlsrv is a driver for connecting to SQL Server"),
+ },
+ {
+ Name: "pdo_sqlsrv",
+ Slug: "pdo_sqlsrv",
+ Description: s.t.Get("pdo_sqlsrv is a PDO driver for connecting to SQL Server"),
+ },
+ {
+ Name: "imap",
+ Slug: "imap",
+ Description: s.t.Get("IMAP module allows PHP to read, search, delete, download, and manage emails"),
+ },
+ {
+ Name: "zip",
+ Slug: "zip",
+ Description: s.t.Get("Zip is a library for handling ZIP files"),
+ },
+ {
+ Name: "bz2",
+ Slug: "bz2",
+ Description: s.t.Get("Bzip2 is a library for compressing and decompressing files"),
+ },
+ {
+ Name: "ssh2",
+ Slug: "ssh2",
+ Description: s.t.Get("SSH2 is a library for connecting to SSH servers"),
+ },
+ {
+ Name: "event",
+ Slug: "event",
+ Description: s.t.Get("Event is a library for handling events"),
+ },
+ {
+ Name: "readline",
+ Slug: "readline",
+ Description: s.t.Get("Readline is a library for processing text"),
+ },
+ {
+ Name: "snmp",
+ Slug: "snmp",
+ Description: s.t.Get("SNMP is a protocol for network management"),
+ },
+ {
+ Name: "ldap",
+ Slug: "ldap",
+ Description: s.t.Get("LDAP is a protocol for accessing directory services"),
+ },
+ {
+ Name: "enchant",
+ Slug: "enchant",
+ Description: s.t.Get("Enchant is a spell-checking library"),
+ },
+ {
+ Name: "pspell",
+ Slug: "pspell",
+ Description: s.t.Get("Pspell is a spell-checking library"),
+ },
+ {
+ Name: "calendar",
+ Slug: "calendar",
+ Description: s.t.Get("Calendar is a library for handling dates"),
+ },
+ {
+ Name: "gmp",
+ Slug: "gmp",
+ Description: s.t.Get("GMP is a library for handling large integers"),
+ },
+ {
+ Name: "xlswriter",
+ Slug: "xlswriter",
+ Description: s.t.Get("XLSWriter is a high-performance library for reading and writing Excel files"),
+ },
+ {
+ Name: "xsl",
+ Slug: "xsl",
+ Description: s.t.Get("XSL is a library for processing XML documents"),
+ },
+ {
+ Name: "intl",
+ Slug: "intl",
+ Description: s.t.Get("Intl is a library for handling internationalization and localization"),
+ },
+ {
+ Name: "gettext",
+ Slug: "gettext",
+ Description: s.t.Get("Gettext is a library for handling multilingual support"),
+ },
+ {
+ Name: "grpc",
+ Slug: "grpc",
+ Description: s.t.Get("gRPC is a high-performance, open-source, and general-purpose RPC framework"),
+ },
+ {
+ Name: "protobuf",
+ Slug: "protobuf",
+ Description: s.t.Get("protobuf is a library for serializing and deserializing data"),
+ },
+ {
+ Name: "rdkafka",
+ Slug: "rdkafka",
+ Description: s.t.Get("rdkafka is a library for connecting to Apache Kafka"),
+ },
+ {
+ Name: "xhprof",
+ Slug: "xhprof",
+ Description: s.t.Get("xhprof is a library for performance profiling"),
+ },
+ {
+ Name: "xdebug",
+ Slug: "xdebug",
+ Description: s.t.Get("xdebug is a library for debugging and profiling PHP code"),
+ },
+ {
+ Name: "yaml",
+ Slug: "yaml",
+ Description: s.t.Get("yaml is a library for handling YAML"),
+ },
+ {
+ Name: "zstd",
+ Slug: "zstd",
+ Description: s.t.Get("zstd is a library for compressing and decompressing files"),
+ },
+ {
+ Name: "sysvmsg",
+ Slug: "sysvmsg",
+ Description: s.t.Get("Sysvmsg is a library for handling System V message queues"),
+ },
+ {
+ Name: "sysvsem",
+ Slug: "sysvsem",
+ Description: s.t.Get("Sysvsem is a library for handling System V semaphores"),
+ },
+ {
+ Name: "sysvshm",
+ Slug: "sysvshm",
+ Description: s.t.Get("Sysvshm is a library for handling System V shared memory"),
+ },
+ {
+ Name: "ionCube",
+ Slug: "ionCube Loader",
+ Description: s.t.Get("ionCube is a professional-grade PHP encryption and decryption tool (must be installed after OPcache)"),
+ },
+ {
+ Name: "Swoole",
+ Slug: "swoole",
+ Description: s.t.Get("Swoole is a PHP module for building high-performance asynchronous concurrent servers"),
+ },
+ }
+
+ // Swow 不支持 PHP 8.0 以下版本且目前不支持 PHP 8.4
+ if version >= 80 && version < 84 {
+ modules = append(modules, types.EnvironmentPHPModule{
+ Name: "Swow",
+ Slug: "Swow",
+ Description: s.t.Get("Swow is a PHP module for building high-performance asynchronous concurrent servers"),
+ })
+ }
+ // PHP 8.4 移除了 pspell 和 imap 并且不再建议使用
+ if version >= 84 {
+ modules = slices.DeleteFunc(modules, func(module types.EnvironmentPHPModule) bool {
+ return module.Slug == "pspell" || module.Slug == "imap"
+ })
+ }
+ // PHP 8.5 原生支持 OPcache,不再作为扩展提供安装
+ if version >= 85 {
+ modules = slices.DeleteFunc(modules, func(module types.EnvironmentPHPModule) bool {
+ return module.Slug == "Zend OPcache"
+ })
+ }
+
+ raw, _ := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, version)
+ moduleMap := make(map[string]*types.EnvironmentPHPModule)
+ for i := range modules {
+ moduleMap[modules[i].Slug] = &modules[i]
+ }
+
+ rawModuleList := strings.Split(raw, "\n")
+ for _, item := range rawModuleList {
+ if ext, exists := moduleMap[item]; exists && !strings.Contains(item, "[") && item != "" {
+ ext.Installed = true
+ }
+ }
+
+ return modules
+}
+
+func (s *EnvironmentPHPService) checkModule(version uint, slug string) bool {
+ modules := s.getModules(version)
+
+ for _, item := range modules {
+ if item.Slug == slug {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/internal/service/service.go b/internal/service/service.go
index 23e01a69..5ea91e58 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -19,6 +19,8 @@ var ProviderSet = wire.NewSet(
NewDatabaseService,
NewDatabaseServerService,
NewDatabaseUserService,
+ NewEnvironmentService,
+ NewEnvironmentPHPService,
NewFileService,
NewFirewallService,
NewHomeService,
diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go
index 9bded602..c5a956e9 100644
--- a/pkg/acme/solvers.go
+++ b/pkg/acme/solvers.go
@@ -39,9 +39,7 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
if err != nil {
return fmt.Errorf("failed to open nginx config %q: %w", s.conf, err)
}
- defer func(file *os.File) {
- _ = file.Close()
- }(file)
+ defer func(file *os.File) { _ = file.Close() }(file)
if _, err = file.Write([]byte(conf)); err != nil {
return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err)
@@ -254,9 +252,7 @@ func (s *manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge)
}
func (s *manualDNSSolver) CleanUp(_ context.Context, _ acme.Challenge) error {
- defer func() {
- _ = recover()
- }()
+ defer func() { _ = recover() }()
close(s.controlChan)
close(s.dnsChan)
close(s.certChan)
diff --git a/pkg/api/app.go b/pkg/api/app.go
index a086cd0b..6b8ce051 100644
--- a/pkg/api/app.go
+++ b/pkg/api/app.go
@@ -9,7 +9,6 @@ type App struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Slug string `json:"slug"`
- Icon string `json:"icon"`
Name string `json:"name"`
Description string `json:"description"`
Categories []string `json:"categories"`
diff --git a/pkg/api/environment.go b/pkg/api/environment.go
new file mode 100644
index 00000000..53b87467
--- /dev/null
+++ b/pkg/api/environment.go
@@ -0,0 +1,47 @@
+package api
+
+import "fmt"
+
+type Environment struct {
+ Type string `json:"type"`
+ Slug string `json:"slug"`
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ Order int `json:"order"`
+}
+
+type Environments []*Environment
+
+// Environments 返回所有环境
+func (r *API) Environments() (*Environments, error) {
+ resp, err := r.client.R().SetResult(&Response{}).Get("/environments")
+ if err != nil {
+ return nil, err
+ }
+ if !resp.IsSuccess() {
+ return nil, fmt.Errorf("failed to get environments: %s", resp.String())
+ }
+
+ environments, err := getResponseData[Environments](resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return environments, nil
+}
+
+// EnvironmentCallback 环境下载回调
+func (r *API) EnvironmentCallback(typ, slug string) error {
+ resp, err := r.client.R().
+ SetResult(&Response{}).
+ Post(fmt.Sprintf("/environments/%s/%s/callback", typ, slug))
+ if err != nil {
+ return err
+ }
+ if !resp.IsSuccess() {
+ return fmt.Errorf("failed to callback environment: %s", resp.String())
+ }
+
+ return nil
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index c284ac88..6ac5763a 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -19,11 +19,13 @@ type Config struct {
}
type AppConfig struct {
- Debug bool `yaml:"debug"`
- Key string `yaml:"key"`
- Locale string `yaml:"locale"`
- Timezone string `yaml:"timezone"`
- Root string `yaml:"root"`
+ Debug bool `yaml:"debug"`
+ Key string `yaml:"key"`
+ Locale string `yaml:"locale"`
+ Timezone string `yaml:"timezone"`
+ Root string `yaml:"root"`
+ APIEndpoint string `yaml:"api_endpoint"`
+ DownloadEndpoint string `yaml:"download_endpoint"`
}
type HTTPConfig struct {
diff --git a/pkg/db/mysql.go b/pkg/db/mysql.go
index 277b8d22..be9a2b77 100644
--- a/pkg/db/mysql.go
+++ b/pkg/db/mysql.go
@@ -77,9 +77,7 @@ func (r *MySQL) DatabaseExists(name string) (bool, error) {
if err != nil {
return false, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
for rows.Next() {
var database string
@@ -223,9 +221,7 @@ func (r *MySQL) Databases() ([]Database, error) {
if err != nil {
return nil, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
var databases []Database
for rows.Next() {
@@ -248,9 +244,7 @@ func (r *MySQL) userGrants(user, host string) ([]string, error) {
if err != nil {
return nil, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
var grants []string
for rows.Next() {
diff --git a/pkg/db/postgres.go b/pkg/db/postgres.go
index 26b5afe0..67bbe75a 100644
--- a/pkg/db/postgres.go
+++ b/pkg/db/postgres.go
@@ -145,9 +145,7 @@ func (r *Postgres) UserPrivileges(user string, host ...string) ([]string, error)
if err != nil {
return nil, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
var databases []string
@@ -197,9 +195,7 @@ func (r *Postgres) Users() ([]User, error) {
if err != nil {
return nil, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
var users []User
for rows.Next() {
@@ -250,9 +246,7 @@ func (r *Postgres) Databases() ([]Database, error) {
if err != nil {
return nil, err
}
- defer func(rows *sql.Rows) {
- _ = rows.Close()
- }(rows)
+ defer func(rows *sql.Rows) { _ = rows.Close() }(rows)
var databases []Database
for rows.Next() {
diff --git a/pkg/io/file.go b/pkg/io/file.go
index 487d2749..8775dd03 100644
--- a/pkg/io/file.go
+++ b/pkg/io/file.go
@@ -35,9 +35,7 @@ func Write(path string, data string, permission os.FileMode) error {
if err != nil {
return err
}
- defer func(file *os.File) {
- _ = file.Close()
- }(file)
+ defer func(file *os.File) { _ = file.Close() }(file)
_, err = file.WriteString(data)
if err != nil {
@@ -73,9 +71,7 @@ func WriteAppend(path string, data string, permission os.FileMode) error {
if err != nil {
return err
}
- defer func(file *os.File) {
- _ = file.Close()
- }(file)
+ defer func(file *os.File) { _ = file.Close() }(file)
_, err = file.WriteString(data)
if err != nil {
diff --git a/pkg/os/os.go b/pkg/os/os.go
index 135b8666..3a407605 100644
--- a/pkg/os/os.go
+++ b/pkg/os/os.go
@@ -13,9 +13,7 @@ func readOSRelease() map[string]string {
if err != nil {
return nil
}
- defer func(file *os.File) {
- _ = file.Close()
- }(file)
+ defer func(file *os.File) { _ = file.Close() }(file)
osRelease := make(map[string]string)
scanner := bufio.NewScanner(file)
@@ -70,9 +68,7 @@ func TCPPortInUse(port uint) bool {
if err != nil {
return true
}
- defer func(conn net.Listener) {
- _ = conn.Close()
- }(conn)
+ defer func(conn net.Listener) { _ = conn.Close() }(conn)
return false
}
@@ -82,8 +78,6 @@ func UDPPortInUse(port uint) bool {
if err != nil {
return true
}
- defer func(conn net.PacketConn) {
- _ = conn.Close()
- }(conn)
+ defer func(conn net.PacketConn) { _ = conn.Close() }(conn)
return false
}
diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go
index 40c54675..4116561c 100644
--- a/pkg/shell/exec.go
+++ b/pkg/shell/exec.go
@@ -203,9 +203,7 @@ func ExecfWithTTY(shell string, args ...any) (string, error) {
if err != nil {
return "", fmt.Errorf("run %s failed", shell)
}
- defer func(f *os.File) {
- _ = f.Close()
- }(f)
+ defer func(f *os.File) { _ = f.Close() }(f)
if _, err = io.Copy(&out, f); ptyError(err) != nil {
return "", fmt.Errorf("run %s failed, out: %s, err: %w", shell, strings.TrimSpace(out.String()), err)
diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go
index 49bba0b1..7faa6471 100644
--- a/pkg/tools/tools.go
+++ b/pkg/tools/tools.go
@@ -159,9 +159,7 @@ func GetLocalIPv4() (string, error) {
if err != nil {
return "", err
}
- defer func(conn stdnet.Conn) {
- _ = conn.Close()
- }(conn)
+ defer func(conn stdnet.Conn) { _ = conn.Close() }(conn)
local := conn.LocalAddr().(*stdnet.UDPAddr)
return local.IP.String(), nil
@@ -173,9 +171,7 @@ func GetLocalIPv6() (string, error) {
if err != nil {
return "", err
}
- defer func(conn stdnet.Conn) {
- _ = conn.Close()
- }(conn)
+ defer func(conn stdnet.Conn) { _ = conn.Close() }(conn)
local := conn.LocalAddr().(*stdnet.UDPAddr)
return local.IP.String(), nil
diff --git a/pkg/types/app.go b/pkg/types/app.go
index 21d9b6df..f584d7d0 100644
--- a/pkg/types/app.go
+++ b/pkg/types/app.go
@@ -7,9 +7,8 @@ type App interface {
Route(r chi.Router)
}
-// AppCenter 应用中心结构
-type AppCenter struct {
- Icon string `json:"icon"`
+// AppDetail 应用详情
+type AppDetail struct {
Name string `json:"name"`
Description string `json:"description"`
Categories []string `json:"categories"`
diff --git a/pkg/types/environment.go b/pkg/types/environment.go
new file mode 100644
index 00000000..8352629b
--- /dev/null
+++ b/pkg/types/environment.go
@@ -0,0 +1,11 @@
+package types
+
+// EnvironmentDetail 环境详情
+type EnvironmentDetail struct {
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Slug string `json:"slug"`
+ Installed bool `json:"installed"`
+ HasUpdate bool `json:"has_update"`
+}
diff --git a/internal/apps/php/types.go b/pkg/types/environment_php.go
similarity index 75%
rename from internal/apps/php/types.go
rename to pkg/types/environment_php.go
index bdfedc16..a58eeb39 100644
--- a/internal/apps/php/types.go
+++ b/pkg/types/environment_php.go
@@ -1,6 +1,6 @@
-package php
+package types
-type Extension struct {
+type EnvironmentPHPModule struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
diff --git a/web/src/api/panel/environment/index.ts b/web/src/api/panel/environment/index.ts
new file mode 100644
index 00000000..530eea56
--- /dev/null
+++ b/web/src/api/panel/environment/index.ts
@@ -0,0 +1,16 @@
+import { http } from '@/utils'
+
+export default {
+ // 获取环境类型列表
+ types: (): any => http.Get('/environment/types'),
+ // 获取环境列表
+ list: (page: number, limit: number, type?: string): any =>
+ http.Get('/environment/list', { params: { page, limit, type } }),
+ // 安装环境
+ install: (type: string, slug: string): any => http.Post('/environment/install', { type, slug }),
+ // 卸载环境
+ uninstall: (type: string, slug: string): any =>
+ http.Post('/environment/uninstall', { type, slug }),
+ // 更新环境
+ update: (type: string, slug: string): any => http.Post('/environment/update', { type, slug })
+}
diff --git a/web/src/utils/common/icon.ts b/web/src/utils/common/icon.ts
index f3ff8ce6..165b524e 100644
--- a/web/src/utils/common/icon.ts
+++ b/web/src/utils/common/icon.ts
@@ -29,7 +29,6 @@ export function renderIcon(icon: string, props: Props = { size: 12 }) {
}
export function renderLocalIcon(type: string, icon: string, props: Props = { size: 12 }) {
- console.log('type, icon', type, icon)
const svgContent = getLocalIconSvg(type, icon)
return () => h(NIcon, { ...props, innerHTML: svgContent })
}
diff --git a/web/src/views/app/EnvironmentView.vue b/web/src/views/app/EnvironmentView.vue
index ea8bac01..aa3d5aed 100644
--- a/web/src/views/app/EnvironmentView.vue
+++ b/web/src/views/app/EnvironmentView.vue
@@ -1,7 +1,244 @@
-
+
-
+
+
+
+ {{ $gettext('All') }}
+
+
+ {{ type.label }}
+
+
+
+
diff --git a/web/src/views/setting/SettingUser.vue b/web/src/views/setting/SettingUser.vue
index d4fba514..1651ed9f 100644
--- a/web/src/views/setting/SettingUser.vue
+++ b/web/src/views/setting/SettingUser.vue
@@ -59,7 +59,6 @@ const columns: any = [
rubberBand: false,
value: row.two_fa !== '',
onUpdateValue: (v) => {
- console.log(v)
if (v) {
twoFaModal.value = true
currentID.value = row.id