2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00

feat: 初步支持环境管理

This commit is contained in:
2026-01-07 01:20:08 +08:00
parent 9f52de654a
commit edadc9b045
41 changed files with 1301 additions and 745 deletions

View File

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

7
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ var ProviderSet = wire.NewSet(
NewDatabaseRepo,
NewDatabaseServerRepo,
NewDatabaseUserRepo,
NewEnvironmentRepo,
NewMonitorRepo,
NewSafeRepo,
NewSettingRepo,

View File

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

View File

@@ -0,0 +1,7 @@
package request
// EnvironmentAction 环境操作请求
type EnvironmentAction struct {
Type string `json:"type"`
Slug string `json:"slug"`
}

View File

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

View File

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

View File

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

View File

@@ -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)
// 随机默认端口

View File

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

View File

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

View File

@@ -19,6 +19,8 @@ var ProviderSet = wire.NewSet(
NewDatabaseService,
NewDatabaseServerService,
NewDatabaseUserService,
NewEnvironmentService,
NewEnvironmentPHPService,
NewFileService,
NewFirewallService,
NewHomeService,

View File

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

View File

@@ -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"`

47
pkg/api/environment.go Normal file
View File

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

View File

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

View File

@@ -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() {

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"`

11
pkg/types/environment.go Normal file
View File

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

View File

@@ -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"`

View File

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

View File

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

View File

@@ -1,7 +1,244 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import environment from '@/api/panel/environment'
import { renderLocalIcon } from '@/utils'
import { NButton, NDataTable, NFlex, NPopconfirm, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
// 当前选中的类型(默认为全部)
const selectedType = ref<string>('')
// 环境类型列表
const { data: types } = useRequest(environment.types, {
initialData: []
})
const columns: any = [
{
key: 'icon',
fixed: 'left',
width: 80,
align: 'center',
render(row: any) {
return renderLocalIcon('environment', row.type, { size: 26 })()
}
},
{
title: $gettext('Name'),
key: 'name',
width: 200,
ellipsis: { tooltip: true }
},
{
title: $gettext('Description'),
key: 'description',
minWidth: 300,
ellipsis: { tooltip: true }
},
{
title: $gettext('Installed Version'),
key: 'installed_version',
width: 160,
ellipsis: { tooltip: true }
},
{
title: $gettext('Actions'),
key: 'actions',
width: 350,
hideInExcel: true,
render(row: any) {
return h(NFlex, null, {
default: () => [
row.installed && row.has_update
? h(
NPopconfirm,
{
onPositiveClick: () => handleUpdate(row.type, row.slug)
},
{
default: () => {
return $gettext('Are you sure to update environment %{ environment }?', {
environment: row.name
})
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'warning'
},
{
default: () => $gettext('Update')
}
)
}
}
)
: null,
row.installed
? h(
NButton,
{
size: 'small',
type: 'success'
//onClick: () => handleManage(row.slug)
},
{
default: () => $gettext('Manage')
}
)
: null,
row.installed
? h(
NPopconfirm,
{
onPositiveClick: () => handleUninstall(row.type, row.slug)
},
{
default: () => {
return $gettext('Are you sure to uninstall environment %{ environment }?', {
environment: row.name
})
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error'
},
{
default: () => $gettext('Uninstall')
}
)
}
}
)
: null,
!row.installed
? h(
NPopconfirm,
{
onPositiveClick: () => handleInstall(row.type, row.slug)
},
{
default: () => {
return $gettext('Are you sure to install environment %{ environment }?', {
environment: row.name
})
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'primary'
},
{
default: () => $gettext('Install')
}
)
}
}
)
: null
]
})
}
}
]
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => environment.list(page, pageSize, selectedType.value || undefined),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,
total: (res: any) => res.total,
data: (res: any) => res.items,
watchingStates: [selectedType]
}
)
// 处理类型切换
const handleTypeChange = (type: string) => {
selectedType.value = type
page.value = 1
}
const handleInstall = (type: string, slug: string) => {
useRequest(environment.install(type, slug)).onSuccess(() => {
window.$message.success(
$gettext('Task submitted, please check the progress in background tasks')
)
})
}
const handleUpdate = (type: string, slug: string) => {
useRequest(environment.update(type, slug)).onSuccess(() => {
window.$message.success(
$gettext('Task submitted, please check the progress in background tasks')
)
})
}
const handleUninstall = (type: string, slug: string) => {
useRequest(environment.uninstall(type, slug)).onSuccess(() => {
window.$message.success(
$gettext('Task submitted, please check the progress in background tasks')
)
})
}
onMounted(() => {
refresh()
})
</script>
<template>
<n-empty></n-empty>
<n-flex vertical>
<n-flex>
<n-tag
:type="selectedType === '' ? 'primary' : 'default'"
:bordered="selectedType !== ''"
style="cursor: pointer"
@click="handleTypeChange('')"
>
{{ $gettext('All') }}
</n-tag>
<n-tag
v-for="type in types"
:key="type.value"
:type="selectedType === type.value ? 'primary' : 'default'"
:bordered="selectedType !== type.value"
style="cursor: pointer"
@click="handleTypeChange(type.value)"
>
{{ type.label }}
</n-tag>
</n-flex>
<n-data-table
striped
remote
:scroll-x="1200"
:loading="loading"
:columns="columns"
:data="data"
:row-key="(row: any) => row.slug"
v-model:page="page"
v-model:pageSize="pageSize"
:pagination="{
page: page,
pageCount: pageCount,
pageSize: pageSize,
itemCount: total,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [20, 50, 100, 200]
}"
/>
</n-flex>
</template>
<style scoped lang="scss"></style>

View File

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