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