From 2d45e84b6686fdbf3428260ec79125428ca0df63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sat, 12 Apr 2025 18:41:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/cli/wire_gen.go | 22 ++++++------ cmd/web/wire_gen.go | 48 ++++++++++++------------- internal/data/cert.go | 27 +++++++------- internal/data/cert_account.go | 28 ++++++++------- internal/data/cert_dns.go | 2 +- internal/data/cron.go | 29 ++++++--------- internal/data/database.go | 7 ++-- internal/data/database_server.go | 16 +++++---- internal/data/database_user.go | 7 ++-- internal/data/monitor.go | 7 +--- internal/data/setting.go | 16 +++++---- internal/data/ssh.go | 12 ++++--- internal/data/task.go | 12 ++++--- internal/data/user.go | 9 +++-- internal/data/website.go | 2 +- internal/service/app.go | 7 ++-- internal/service/backup.go | 13 ++++--- internal/service/cert.go | 45 ++++++++++++----------- internal/service/cli.go | 62 +++++++++++++++++--------------- internal/service/dashboard.go | 47 ++++++++++++------------ internal/service/file.go | 29 ++++++++------- internal/service/systemctl.go | 25 +++++++------ internal/service/user.go | 7 ++-- internal/service/ws.go | 11 +++--- 24 files changed, 267 insertions(+), 223 deletions(-) diff --git a/cmd/cli/wire_gen.go b/cmd/cli/wire_gen.go index 82abe325..2febb198 100644 --- a/cmd/cli/wire_gen.go +++ b/cmd/cli/wire_gen.go @@ -61,18 +61,18 @@ func initCli() (*app.Cli, error) { } cacheRepo := data.NewCacheRepo(db) queue := bootstrap.NewQueue() - taskRepo := data.NewTaskRepo(db, logger, queue) - appRepo := data.NewAppRepo(db, cacheRepo, taskRepo) - userRepo := data.NewUserRepo(db) - settingRepo := data.NewSettingRepo(db, koanf, taskRepo) - databaseServerRepo := data.NewDatabaseServerRepo(db, logger) - databaseUserRepo := data.NewDatabaseUserRepo(db, databaseServerRepo) - databaseRepo := data.NewDatabaseRepo(db, databaseServerRepo, databaseUserRepo) - certRepo := data.NewCertRepo(db, logger) - certAccountRepo := data.NewCertAccountRepo(db, userRepo, logger) + taskRepo := data.NewTaskRepo(locale, db, logger, queue) + appRepo := data.NewAppRepo(locale, db, cacheRepo, taskRepo) + userRepo := data.NewUserRepo(locale, db) + settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo) + databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger) + databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo) + databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo) + certRepo := data.NewCertRepo(locale, db, logger) + certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger) websiteRepo := data.NewWebsiteRepo(db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo) - backupRepo := data.NewBackupRepo(db, settingRepo, websiteRepo) - cliService := service.NewCliService(koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo) + backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo) + cliService := service.NewCliService(locale, koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo) cli := route.NewCli(cliService) command := bootstrap.NewCli(locale, cli) gormigrate := bootstrap.NewMigrate(db) diff --git a/cmd/web/wire_gen.go b/cmd/web/wire_gen.go index 76124c2c..c955aa48 100644 --- a/cmd/web/wire_gen.go +++ b/cmd/web/wire_gen.go @@ -61,40 +61,44 @@ func initWeb() (*app.Web, error) { if err != nil { return nil, err } + locale, err := bootstrap.NewT(koanf) + if err != nil { + return nil, err + } cacheRepo := data.NewCacheRepo(db) queue := bootstrap.NewQueue() - taskRepo := data.NewTaskRepo(db, logger, queue) - appRepo := data.NewAppRepo(db, cacheRepo, taskRepo) + taskRepo := data.NewTaskRepo(locale, db, logger, queue) + appRepo := data.NewAppRepo(locale, db, cacheRepo, taskRepo) middlewares := middleware.NewMiddlewares(koanf, logger, manager, appRepo) - userRepo := data.NewUserRepo(db) - userService := service.NewUserService(koanf, manager, userRepo) - databaseServerRepo := data.NewDatabaseServerRepo(db, logger) - databaseUserRepo := data.NewDatabaseUserRepo(db, databaseServerRepo) - databaseRepo := data.NewDatabaseRepo(db, databaseServerRepo, databaseUserRepo) - certRepo := data.NewCertRepo(db, logger) - certAccountRepo := data.NewCertAccountRepo(db, userRepo, logger) + userRepo := data.NewUserRepo(locale, db) + userService := service.NewUserService(locale, koanf, manager, userRepo) + databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger) + databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo) + databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo) + certRepo := data.NewCertRepo(locale, db, logger) + certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger) websiteRepo := data.NewWebsiteRepo(db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo) - settingRepo := data.NewSettingRepo(db, koanf, taskRepo) - cronRepo := data.NewCronRepo(db) - backupRepo := data.NewBackupRepo(db, settingRepo, websiteRepo) - dashboardService := service.NewDashboardService(koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo) + settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo) + cronRepo := data.NewCronRepo(locale, db) + backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo) + dashboardService := service.NewDashboardService(locale, koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo) taskService := service.NewTaskService(taskRepo) websiteService := service.NewWebsiteService(websiteRepo, settingRepo) databaseService := service.NewDatabaseService(databaseRepo) databaseServerService := service.NewDatabaseServerService(databaseServerRepo) databaseUserService := service.NewDatabaseUserService(databaseUserRepo) - backupService := service.NewBackupService(backupRepo) - certService := service.NewCertService(certRepo) + backupService := service.NewBackupService(locale, backupRepo) + certService := service.NewCertService(locale, certRepo) certDNSRepo := data.NewCertDNSRepo(db) certDNSService := service.NewCertDNSService(certDNSRepo) certAccountService := service.NewCertAccountService(certAccountRepo) - appService := service.NewAppService(appRepo, cacheRepo, settingRepo) + appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo) cronService := service.NewCronService(cronRepo) processService := service.NewProcessService() safeRepo := data.NewSafeRepo() safeService := service.NewSafeService(safeRepo) firewallService := service.NewFirewallService() - sshRepo := data.NewSSHRepo(db) + sshRepo := data.NewSSHRepo(locale, db) sshService := service.NewSSHService(sshRepo) containerRepo := data.NewContainerRepo() containerService := service.NewContainerService(containerRepo) @@ -106,15 +110,11 @@ func initWeb() (*app.Web, error) { containerImageService := service.NewContainerImageService(containerImageRepo) containerVolumeRepo := data.NewContainerVolumeRepo() containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo) - fileService := service.NewFileService(taskRepo) + fileService := service.NewFileService(locale, taskRepo) monitorRepo := data.NewMonitorRepo(db, settingRepo) monitorService := service.NewMonitorService(settingRepo, monitorRepo) settingService := service.NewSettingService(settingRepo) - systemctlService := service.NewSystemctlService() - locale, err := bootstrap.NewT(koanf) - if err != nil { - return nil, err - } + systemctlService := service.NewSystemctlService(locale) benchmarkApp := benchmark.NewApp(locale) dockerApp := docker.NewApp() fail2banApp := fail2ban.NewApp(locale, websiteRepo) @@ -141,7 +141,7 @@ func initWeb() (*app.Web, error) { toolboxApp := toolbox.NewApp(locale) loader := bootstrap.NewLoader(benchmarkApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, minioApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp, toolboxApp) http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader) - wsService := service.NewWsService(koanf, sshRepo) + wsService := service.NewWsService(locale, koanf, sshRepo) ws := route.NewWs(wsService) mux, err := bootstrap.NewRouter(middlewares, http, ws) if err != nil { diff --git a/internal/data/cert.go b/internal/data/cert.go index a14414b3..ea0b032f 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/app" @@ -24,13 +25,15 @@ import ( ) type certRepo struct { + t *gotext.Locale db *gorm.DB log *slog.Logger client *acme.Client } -func NewCertRepo(db *gorm.DB, log *slog.Logger) biz.CertRepo { +func NewCertRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.CertRepo { return &certRepo{ + t: t, db: db, log: log, } @@ -86,10 +89,10 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) { func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) { info, err := pkgcert.ParseCert(req.Cert) if err != nil { - return nil, fmt.Errorf("failed to parse certificate: %v", err) + return nil, errors.New(r.t.Get("failed to parse certificate: %v", err)) } if _, err = pkgcert.ParseKey(req.Key); err != nil { - return nil, fmt.Errorf("failed to parse private key: %v", err) + return nil, errors.New(r.t.Get("failed to parse private key: %v", err)) } cert := &biz.Cert{ @@ -126,7 +129,7 @@ func (r *certRepo) Update(req *request.CertUpdate) error { req.Domains = info.DNSNames } if req.Type == "upload" && req.AutoRenew { - return errors.New("upload certificate cannot be set to auto renew") + return errors.New(r.t.Get("upload certificate cannot be set to auto renew")) } return r.db.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{ @@ -162,11 +165,11 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) { client.UseDns(cert.DNS.Type, cert.DNS.Data) } else { if cert.Website == nil { - return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually") + return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")) } else { for _, domain := range cert.Domains { if strings.Contains(domain, "*") { - return nil, errors.New("wildcard domains cannot use HTTP verification") + return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification")) } } conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) @@ -204,7 +207,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) { } if r.client == nil { - return nil, errors.New("please retry the manual obtain operation") + return nil, errors.New(r.t.Get("please retry the manual obtain operation")) } ssl, err := r.client.ObtainCertificateManual() @@ -270,18 +273,18 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { } if cert.CertURL == "" { - return nil, errors.New("this certificate has not been signed successfully and cannot be renewed") + return nil, errors.New(r.t.Get("this certificate has not been signed successfully and cannot be renewed")) } if cert.DNS != nil { client.UseDns(cert.DNS.Type, cert.DNS.Data) } else { if cert.Website == nil { - return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually") + return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")) } else { for _, domain := range cert.Domains { if strings.Contains(domain, "*") { - return nil, errors.New("wildcard domains cannot use HTTP verification") + return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification")) } } conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) @@ -345,7 +348,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error { } if cert.Cert == "" || cert.Key == "" { - return errors.New("this certificate has not been signed successfully and cannot be deployed") + return errors.New(r.t.Get("this certificate has not been signed successfully and cannot be deployed")) } website := new(biz.Website) @@ -400,7 +403,7 @@ func (r *certRepo) runScript(cert *biz.Cert) error { func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) { if cert.Account == nil { - return nil, errors.New("this certificate is not associated with an ACME account and cannot be signed") + return nil, errors.New(r.t.Get("this certificate is not associated with an ACME account and cannot be signed")) } var ca string diff --git a/internal/data/cert_account.go b/internal/data/cert_account.go index 8a919867..dcaa12d1 100644 --- a/internal/data/cert_account.go +++ b/internal/data/cert_account.go @@ -3,11 +3,11 @@ package data import ( "context" "errors" - "fmt" "log/slog" "time" "github.com/go-resty/resty/v2" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -17,13 +17,15 @@ import ( ) type certAccountRepo struct { + t *gotext.Locale db *gorm.DB log *slog.Logger user biz.UserRepo } -func NewCertAccountRepo(db *gorm.DB, user biz.UserRepo, log *slog.Logger) biz.CertAccountRepo { +func NewCertAccountRepo(t *gotext.Locale, db *gorm.DB, user biz.UserRepo, log *slog.Logger) biz.CertAccountRepo { return &certAccountRepo{ + t: t, db: db, log: log, user: user, @@ -31,7 +33,7 @@ func NewCertAccountRepo(db *gorm.DB, user biz.UserRepo, log *slog.Logger) biz.Ce } func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, error) { - var accounts []*biz.CertAccount + accounts := make([]*biz.CertAccount, 0) var total int64 err := r.db.Model(&biz.CertAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&accounts).Error return accounts, total, err @@ -99,16 +101,16 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun case "sslcom": client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log) default: - return nil, errors.New("unsupported CA") + return nil, errors.New(r.t.Get("unsupported CA")) } if err != nil { - return nil, fmt.Errorf("failed to register account: %v", err) + return nil, errors.New(r.t.Get("failed to register account: %v", err)) } privateKey, err := cert.EncodeKey(client.Account.PrivateKey) if err != nil { - return nil, errors.New("failed to get private key") + return nil, errors.New(r.t.Get("failed to get private key")) } account.PrivateKey = string(privateKey) @@ -158,16 +160,16 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error { case "sslcom": client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log) default: - return errors.New("unsupported CA") + return errors.New(r.t.Get("unsupported CA")) } if err != nil { - return errors.New("failed to register account") + return errors.New(r.t.Get("failed to register account: %v", err)) } privateKey, err := cert.EncodeKey(client.Account.PrivateKey) if err != nil { - return errors.New("failed to get private key") + return errors.New(r.t.Get("failed to get private key: %v", err)) } account.PrivateKey = string(privateKey) @@ -193,11 +195,11 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) { resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab") if err != nil || !resp.IsSuccess() { - return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %v", err) + return &acme.EAB{}, errors.New(r.t.Get("failed to get Google EAB: %v", err)) } eab := resp.Result().(*data) if eab.Message != "success" { - return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %s", eab.Message) + return &acme.EAB{}, errors.New(r.t.Get("failed to get Google EAB: %s", eab.Message)) } return &acme.EAB{KeyID: eab.Data.KeyId, MACKey: eab.Data.MacKey}, nil @@ -218,11 +220,11 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) { "email": email, }).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email") if err != nil || !resp.IsSuccess() { - return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB: %v", err) + return &acme.EAB{}, errors.New(r.t.Get("failed to get ZeroSSL EAB: %v", err)) } eab := resp.Result().(*data) if !eab.Success { - return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB") + return &acme.EAB{}, errors.New(r.t.Get("failed to get ZeroSSL EAB")) } return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil diff --git a/internal/data/cert_dns.go b/internal/data/cert_dns.go index 93d9c1a7..467b0d45 100644 --- a/internal/data/cert_dns.go +++ b/internal/data/cert_dns.go @@ -18,7 +18,7 @@ func NewCertDNSRepo(db *gorm.DB) biz.CertDNSRepo { } func (r certDNSRepo) List(page, limit uint) ([]*biz.CertDNS, int64, error) { - var certDNS []*biz.CertDNS + certDNS := make([]*biz.CertDNS, 0) var total int64 err := r.db.Model(&biz.CertDNS{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certDNS).Error return certDNS, total, err diff --git a/internal/data/cron.go b/internal/data/cron.go index b8a1eaf7..ad6c0986 100644 --- a/internal/data/cron.go +++ b/internal/data/cron.go @@ -8,6 +8,7 @@ import ( "time" "github.com/go-rat/utils/str" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/app" @@ -20,11 +21,13 @@ import ( ) type cronRepo struct { + t *gotext.Locale db *gorm.DB } -func NewCronRepo(db *gorm.DB) biz.CronRepo { +func NewCronRepo(t *gotext.Locale, db *gorm.DB) biz.CronRepo { return &cronRepo{ + t: t, db: db, } } @@ -39,7 +42,7 @@ func (r *cronRepo) Count() (int64, error) { } func (r *cronRepo) List(page, limit uint) ([]*biz.Cron, int64, error) { - var cron []*biz.Cron + cron := make([]*biz.Cron, 0) var total int64 err := r.db.Model(&biz.Cron{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&cron).Error return cron, total, err @@ -61,8 +64,6 @@ func (r *cronRepo) Create(req *request.CronCreate) error { script = fmt.Sprintf(`#!/bin/bash export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH -# 耗子面板 - 网站备份脚本 - panel-cli backup website -n '%s' -p '%s' panel-cli backup clear -t website -f '%s' -s '%d' -p '%s' systemctl reload nginx @@ -72,8 +73,6 @@ systemctl reload nginx script = fmt.Sprintf(`#!/bin/bash export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH -# 耗子面板 - 数据库备份脚本 - panel-cli backup database -t '%s' -n '%s' -p '%s' panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s' `, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath) @@ -83,9 +82,6 @@ panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s' script = fmt.Sprintf(`#!/bin/bash export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH -# 耗子面板 - 日志切割脚本 - -# 执行切割 panel-cli cutoff website -n '%s' -p '%s' panel-cli cutoff clear -t website -f '%s' -s '%d' -p '%s' `, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath) @@ -97,18 +93,17 @@ panel-cli cutoff clear -t website -f '%s' -s '%d' -p '%s' shellDir := fmt.Sprintf("%s/server/cron/", app.Root) shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root) if !io.Exists(shellDir) { - return errors.New("计划任务目录不存在") + return errors.New(r.t.Get("cron directory %s not exists", shellDir)) } if !io.Exists(shellLogDir) { - return errors.New("计划任务日志目录不存在") + return errors.New(r.t.Get("cron log directory %s not exists", shellLogDir)) } shellFile := strconv.Itoa(int(time.Now().Unix())) + str.Random(16) if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil { return errors.New(err.Error()) } - if out, err := shell.Execf("dos2unix %s%s.sh", shellDir, shellFile); err != nil { - return errors.New(out) - } + // 编码转换 + _, _ = shell.Execf("dos2unix %s%s.sh", shellDir, shellFile) cron := new(biz.Cron) cron.Name = req.Name @@ -134,10 +129,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error { return err } - if !cron.Status { - return errors.New("计划任务已禁用") - } - cron.Time = req.Time cron.Name = req.Name if err = r.db.Save(cron).Error; err != nil { @@ -227,5 +218,5 @@ func (r *cronRepo) restartCron() error { return systemctl.Restart("cron") } - return errors.New("不支持的系统") + return errors.New(r.t.Get("unsupported system")) } diff --git a/internal/data/database.go b/internal/data/database.go index 03828a80..9c072cf1 100644 --- a/internal/data/database.go +++ b/internal/data/database.go @@ -5,6 +5,7 @@ import ( "fmt" "slices" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -13,13 +14,15 @@ import ( ) type databaseRepo struct { + t *gotext.Locale db *gorm.DB server biz.DatabaseServerRepo user biz.DatabaseUserRepo } -func NewDatabaseRepo(db *gorm.DB, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo { +func NewDatabaseRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo { return &databaseRepo{ + t: t, db: db, server: server, user: user, @@ -179,7 +182,7 @@ func (r databaseRepo) Comment(req *request.DatabaseComment) error { switch server.Type { case biz.DatabaseTypeMysql: - return errors.New("mysql not support database comment") + return errors.New(r.t.Get("mysql not support database comment")) case biz.DatabaseTypePostgresql: postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) if err != nil { diff --git a/internal/data/database_server.go b/internal/data/database_server.go index a87488e8..f36feaa3 100644 --- a/internal/data/database_server.go +++ b/internal/data/database_server.go @@ -1,10 +1,12 @@ package data import ( + "errors" "fmt" "log/slog" "slices" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -13,12 +15,14 @@ import ( ) type databaseServerRepo struct { + t *gotext.Locale db *gorm.DB log *slog.Logger } -func NewDatabaseServerRepo(db *gorm.DB, log *slog.Logger) biz.DatabaseServerRepo { +func NewDatabaseServerRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.DatabaseServerRepo { return &databaseServerRepo{ + t: t, db: db, log: log, } @@ -34,7 +38,7 @@ func (r databaseServerRepo) Count() (int64, error) { } func (r databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) { - var databaseServer []*biz.DatabaseServer + databaseServer := make([]*biz.DatabaseServer, 0) var total int64 err := r.db.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error @@ -79,7 +83,7 @@ func (r databaseServerRepo) Create(req *request.DatabaseServerCreate) error { } if !r.checkServer(databaseServer) { - return fmt.Errorf("check server connection failed") + return errors.New(r.t.Get("check server connection failed")) } return r.db.Create(databaseServer).Error @@ -99,7 +103,7 @@ func (r databaseServerRepo) Update(req *request.DatabaseServerUpdate) error { server.Remark = req.Remark if !r.checkServer(server) { - return fmt.Errorf("check server connection failed") + return errors.New(r.t.Get("check server connection failed")) } return r.db.Save(server).Error @@ -154,7 +158,7 @@ func (r databaseServerRepo) Sync(id uint) error { ServerID: id, Username: user.User, Host: user.Host, - Remark: fmt.Sprintf("sync from server %s", server.Name), + Remark: r.t.Get("sync from server %s", server.Name), } if err = r.db.Create(newUser).Error; err != nil { r.log.Warn("sync database user failed", slog.Any("err", err)) @@ -180,7 +184,7 @@ func (r databaseServerRepo) Sync(id uint) error { newUser := &biz.DatabaseUser{ ServerID: id, Username: user.Role, - Remark: fmt.Sprintf("sync from server %s", server.Name), + Remark: r.t.Get("sync from server %s", server.Name), } r.db.Create(newUser) } diff --git a/internal/data/database_user.go b/internal/data/database_user.go index 58edec04..6a570cf2 100644 --- a/internal/data/database_user.go +++ b/internal/data/database_user.go @@ -4,6 +4,7 @@ import ( "fmt" "slices" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -12,12 +13,14 @@ import ( ) type databaseUserRepo struct { + t *gotext.Locale db *gorm.DB server biz.DatabaseServerRepo } -func NewDatabaseUserRepo(db *gorm.DB, server biz.DatabaseServerRepo) biz.DatabaseUserRepo { +func NewDatabaseUserRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo) biz.DatabaseUserRepo { return &databaseUserRepo{ + t: t, db: db, server: server, } @@ -33,7 +36,7 @@ func (r databaseUserRepo) Count() (int64, error) { } func (r databaseUserRepo) List(page, limit uint) ([]*biz.DatabaseUser, int64, error) { - var user []*biz.DatabaseUser + user := make([]*biz.DatabaseUser, 0) var total int64 err := r.db.Model(&biz.DatabaseUser{}).Preload("Server").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error diff --git a/internal/data/monitor.go b/internal/data/monitor.go index 8f7144df..765112c0 100644 --- a/internal/data/monitor.go +++ b/internal/data/monitor.go @@ -1,7 +1,6 @@ package data import ( - "errors" "time" "github.com/spf13/cast" @@ -56,14 +55,10 @@ func (r monitorRepo) Clear() error { } func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) { - var monitors []*biz.Monitor + monitors := make([]*biz.Monitor, 0) if err := r.db.Where("created_at BETWEEN ? AND ?", start, end).Find(&monitors).Error; err != nil { return nil, err } - if len(monitors) == 0 { - return nil, errors.New("没有找到数据") - } - return monitors, nil } diff --git a/internal/data/setting.go b/internal/data/setting.go index 65db8c32..6e72f79e 100644 --- a/internal/data/setting.go +++ b/internal/data/setting.go @@ -3,11 +3,11 @@ package data import ( "context" "errors" - "fmt" "path/filepath" "github.com/go-rat/utils/hash" "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" "github.com/spf13/cast" "gopkg.in/yaml.v3" "gorm.io/gorm" @@ -23,13 +23,15 @@ import ( ) type settingRepo struct { + t *gotext.Locale db *gorm.DB conf *koanf.Koanf task biz.TaskRepo } -func NewSettingRepo(db *gorm.DB, conf *koanf.Koanf, task biz.TaskRepo) biz.SettingRepo { +func NewSettingRepo(t *gotext.Locale, db *gorm.DB, conf *koanf.Koanf, task biz.TaskRepo) biz.SettingRepo { return &settingRepo{ + t: t, db: db, conf: conf, task: task, @@ -186,15 +188,15 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key")) if oldCert != setting.Cert || oldKey != setting.Key { if r.task.HasRunningTask() { - return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试") + return false, errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later")) } restartFlag = true } if _, err := cert.ParseCert(setting.Cert); err != nil { - return false, fmt.Errorf("failed to parse certificate: %w", err) + return false, errors.New(r.t.Get("failed to parse certificate: %w", err)) } if _, err := cert.ParseKey(setting.Key); err != nil { - return false, fmt.Errorf("failed to parse private key: %w", err) + return false, errors.New(r.t.Get("failed to parse private key: %w", err)) } if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil { return false, err @@ -215,7 +217,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P if setting.Port != config.HTTP.Port { if os.TCPPortInUse(setting.Port) { - return false, errors.New("端口已被占用") + return false, errors.New(r.t.Get("port is already in use")) } } @@ -243,7 +245,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P } if raw != string(encoded) { if r.task.HasRunningTask() { - return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试") + return false, errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later")) } restartFlag = true } diff --git a/internal/data/ssh.go b/internal/data/ssh.go index 423b9d97..546968bf 100644 --- a/internal/data/ssh.go +++ b/internal/data/ssh.go @@ -1,8 +1,10 @@ package data import ( + "errors" "fmt" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -11,17 +13,19 @@ import ( ) type sshRepo struct { + t *gotext.Locale db *gorm.DB } -func NewSSHRepo(db *gorm.DB) biz.SSHRepo { +func NewSSHRepo(t *gotext.Locale, db *gorm.DB) biz.SSHRepo { return &sshRepo{ + t: t, db: db, } } func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) { - var ssh []*biz.SSH + ssh := make([]*biz.SSH, 0) var total int64 err := r.db.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error return ssh, total, err @@ -46,7 +50,7 @@ func (r *sshRepo) Create(req *request.SSHCreate) error { } _, err := pkgssh.NewSSHClient(conf) if err != nil { - return fmt.Errorf("failed to check ssh connection: %v", err) + return errors.New(r.t.Get("failed to check ssh connection: %v", err)) } ssh := &biz.SSH{ @@ -70,7 +74,7 @@ func (r *sshRepo) Update(req *request.SSHUpdate) error { } _, err := pkgssh.NewSSHClient(conf) if err != nil { - return fmt.Errorf("failed to check ssh connection: %v", err) + return errors.New(r.t.Get("failed to check ssh connection: %v", err)) } ssh := &biz.SSH{ diff --git a/internal/data/task.go b/internal/data/task.go index b80e3fcc..c70ecc9f 100644 --- a/internal/data/task.go +++ b/internal/data/task.go @@ -1,9 +1,10 @@ package data import ( - "fmt" + "errors" "log/slog" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" @@ -12,13 +13,15 @@ import ( ) type taskRepo struct { + t *gotext.Locale db *gorm.DB log *slog.Logger queue *queue.Queue } -func NewTaskRepo(db *gorm.DB, log *slog.Logger, queue *queue.Queue) biz.TaskRepo { +func NewTaskRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, queue *queue.Queue) biz.TaskRepo { return &taskRepo{ + t: t, db: db, log: log, queue: queue, @@ -32,7 +35,7 @@ func (r *taskRepo) HasRunningTask() bool { } func (r *taskRepo) List(page, limit uint) ([]*biz.Task, int64, error) { - var tasks []*biz.Task + tasks := make([]*biz.Task, 0) var total int64 err := r.db.Model(&biz.Task{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&tasks).Error return tasks, total, err @@ -53,12 +56,13 @@ func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error { } func (r *taskRepo) Push(task *biz.Task) error { + // 防止有人喜欢酒吧点炒饭 var count int64 if err := r.db.Model(&biz.Task{}).Where("shell = ? and (status = ? or status = ?)", task.Shell, biz.TaskStatusWaiting, biz.TaskStatusRunning).Count(&count).Error; err != nil { return err } if count > 0 { - return fmt.Errorf("duplicate submission, please wait for the previous task to end") + return errors.New(r.t.Get("duplicate submission, please wait for the previous task to end")) } if err := r.db.Create(task).Error; err != nil { diff --git a/internal/data/user.go b/internal/data/user.go index 62eb48cc..a0aaa73d 100644 --- a/internal/data/user.go +++ b/internal/data/user.go @@ -4,18 +4,21 @@ import ( "errors" "github.com/go-rat/utils/hash" + "github.com/leonelquinteros/gotext" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/biz" ) type userRepo struct { + t *gotext.Locale db *gorm.DB hasher hash.Hasher } -func NewUserRepo(db *gorm.DB) biz.UserRepo { +func NewUserRepo(t *gotext.Locale, db *gorm.DB) biz.UserRepo { return &userRepo{ + t: t, db: db, hasher: hash.NewArgon2id(), } @@ -42,14 +45,14 @@ func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) { user := new(biz.User) if err := r.db.Where("username = ?", username).First(user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("用户名或密码错误") + return nil, errors.New(r.t.Get("username or password error")) } else { return nil, err } } if !r.hasher.Check(password, user.Password) { - return nil, errors.New("用户名或密码错误") + return nil, errors.New(r.t.Get("username or password error")) } return user, nil diff --git a/internal/data/website.go b/internal/data/website.go index 5d17818f..2cd3b6c2 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -208,7 +208,7 @@ func (r *websiteRepo) GetByName(name string) (*types.WebsiteSetting, error) { } func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { - var websites []*biz.Website + websites := make([]*biz.Website, 0) var total int64 if err := r.db.Model(&biz.Website{}).Count(&total).Error; err != nil { diff --git a/internal/service/app.go b/internal/service/app.go index c303318f..5b6d166e 100644 --- a/internal/service/app.go +++ b/internal/service/app.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/go-rat/chix" + "github.com/leonelquinteros/gotext" "github.com/tnb-labs/panel/internal/biz" "github.com/tnb-labs/panel/internal/http/request" @@ -11,13 +12,15 @@ import ( ) type AppService struct { + t *gotext.Locale appRepo biz.AppRepo cacheRepo biz.CacheRepo settingRepo biz.SettingRepo } -func NewAppService(app biz.AppRepo, cache biz.CacheRepo, setting biz.SettingRepo) *AppService { +func NewAppService(t *gotext.Locale, app biz.AppRepo, cache biz.CacheRepo, setting biz.SettingRepo) *AppService { return &AppService{ + t: t, appRepo: app, cacheRepo: cache, settingRepo: setting, @@ -167,7 +170,7 @@ func (s *AppService) IsInstalled(w http.ResponseWriter, r *http.Request) { func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { - Error(w, http.StatusForbidden, "离线模式下无法更新应用列表缓存") + Error(w, http.StatusForbidden, s.t.Get("Unable to update app list cache in offline mode")) return } diff --git a/internal/service/backup.go b/internal/service/backup.go index ef468cb9..3b267b2a 100644 --- a/internal/service/backup.go +++ b/internal/service/backup.go @@ -8,6 +8,7 @@ import ( "slices" "github.com/go-rat/chix" + "github.com/leonelquinteros/gotext" "github.com/tnb-labs/panel/internal/biz" "github.com/tnb-labs/panel/internal/http/request" @@ -15,11 +16,13 @@ import ( ) type BackupService struct { + t *gotext.Locale backupRepo biz.BackupRepo } -func NewBackupService(backup biz.BackupRepo) *BackupService { +func NewBackupService(t *gotext.Locale, backup biz.BackupRepo) *BackupService { return &BackupService{ + t: t, backupRepo: backup, } } @@ -71,7 +74,7 @@ func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) { // 只允许上传 .sql .zip .tar .gz .tgz .bz2 .xz .7z if !slices.Contains([]string{".sql", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z"}, filepath.Ext(req.File.Filename)) { - Error(w, http.StatusForbidden, "unsupported file type") + Error(w, http.StatusForbidden, s.t.Get("unsupported file type")) return } @@ -81,19 +84,19 @@ func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) { return } if io.Exists(filepath.Join(path, req.File.Filename)) { - Error(w, http.StatusForbidden, "target backup %s already exists", path) + Error(w, http.StatusForbidden, s.t.Get("target backup %s already exists", path)) return } src, _ := req.File.Open() out, err := os.OpenFile(filepath.Join(path, req.File.Filename), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) if err != nil { - Error(w, http.StatusInternalServerError, "open file error: %v", err) + Error(w, http.StatusInternalServerError, "%v", err) return } if _, err = stdio.Copy(out, src); err != nil { - Error(w, http.StatusInternalServerError, "write file error: %v", err) + Error(w, http.StatusInternalServerError, "%v", err) return } diff --git a/internal/service/cert.go b/internal/service/cert.go index f64217f1..e8a809cf 100644 --- a/internal/service/cert.go +++ b/internal/service/cert.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/go-rat/chix" + "github.com/leonelquinteros/gotext" "github.com/tnb-labs/panel/internal/biz" "github.com/tnb-labs/panel/internal/http/request" @@ -12,21 +13,19 @@ import ( ) type CertService struct { + t *gotext.Locale certRepo biz.CertRepo } -func NewCertService(cert biz.CertRepo) *CertService { +func NewCertService(t *gotext.Locale, cert biz.CertRepo) *CertService { return &CertService{ + t: t, certRepo: cert, } } func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { Success(w, []types.LV{ - { - Label: "GoogleCN(推荐)", - Value: "googlecn", - }, { Label: "Let's Encrypt", Value: "letsencrypt", @@ -39,6 +38,10 @@ func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { Label: "SSL.com", Value: "sslcom", }, + { + Label: "GoogleCN", + Value: "googlecn", + }, { Label: "Google", Value: "google", @@ -54,67 +57,67 @@ func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) { Success(w, []types.LV{ { - Label: "阿里云", + Label: s.t.Get("Aliyun"), Value: string(acme.AliYun), }, { - Label: "腾讯云", + Label: s.t.Get("Tencent Cloud"), Value: string(acme.Tencent), }, { - Label: "华为云", + Label: s.t.Get("Huawei Cloud"), Value: string(acme.Huawei), }, { - Label: "西部数码", + Label: s.t.Get("West.cn"), Value: string(acme.Westcn), }, { - Label: "CloudFlare", + Label: s.t.Get("CloudFlare"), Value: string(acme.CloudFlare), }, { - Label: "Godaddy", + Label: s.t.Get("Godaddy"), Value: string(acme.Godaddy), }, { - Label: "Gcore", + Label: s.t.Get("Gcore"), Value: string(acme.Gcore), }, { - Label: "Porkbun", + Label: s.t.Get("Porkbun"), Value: string(acme.Porkbun), }, { - Label: "Namecheap", + Label: s.t.Get("Namecheap"), Value: string(acme.Namecheap), }, { - Label: "NameSilo", + Label: s.t.Get("NameSilo"), Value: string(acme.NameSilo), }, { - Label: "Name.com", + Label: s.t.Get("Name.com"), Value: string(acme.Namecom), }, { - Label: "ClouDNS", + Label: s.t.Get("ClouDNS"), Value: string(acme.ClouDNS), }, { - Label: "Duck DNS", + Label: s.t.Get("Duck DNS"), Value: string(acme.DuckDNS), }, { - Label: "Hetzner", + Label: s.t.Get("Hetzner"), Value: string(acme.Hetzner), }, { - Label: "Linode", + Label: s.t.Get("Linode"), Value: string(acme.Linode), }, { - Label: "Vercel", + Label: s.t.Get("Vercel"), Value: string(acme.Vercel), }, }) diff --git a/internal/service/cli.go b/internal/service/cli.go index 7d02e3f2..a307d67f 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -11,6 +11,7 @@ import ( "github.com/go-rat/utils/hash" "github.com/go-rat/utils/str" "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" "github.com/spf13/cast" "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" @@ -32,6 +33,7 @@ import ( type CliService struct { hr string + t *gotext.Locale api *api.API conf *koanf.Koanf db *gorm.DB @@ -45,10 +47,11 @@ type CliService struct { hash hash.Hasher } -func NewCliService(conf *koanf.Koanf, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo) *CliService { +func NewCliService(t *gotext.Locale, conf *koanf.Koanf, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo) *CliService { return &CliService{ hr: `+----------------------------------------------------`, api: api.NewAPI(app.Version), + t: t, conf: conf, db: db, appRepo: appRepo, @@ -67,7 +70,7 @@ func (s *CliService) Restart(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println("面板服务已重启") + fmt.Println(s.t.Get("Panel service restarted")) return nil } @@ -76,7 +79,7 @@ func (s *CliService) Stop(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println("面板服务已停止") + fmt.Println(s.t.Get("Panel service stopped")) return nil } @@ -85,19 +88,19 @@ func (s *CliService) Start(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println("面板服务已启动") + fmt.Println(s.t.Get("Panel service started")) return nil } func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error { panel, err := s.api.LatestVersion() if err != nil { - return fmt.Errorf("获取最新版本失败:%v", err) + return errors.New(s.t.Get("Failed to get latest version: %v", err)) } download := collect.First(panel.Downloads) if download == nil { - return fmt.Errorf("下载地址为空") + return errors.New(s.t.Get("Download URL is empty")) } return s.backupRepo.UpdatePanel(panel.Version, download.URL, download.Checksum) @@ -105,13 +108,14 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error { func (s *CliService) Sync(ctx context.Context, cmd *cli.Command) error { if err := s.cacheRepo.UpdateApps(); err != nil { - return fmt.Errorf("同步应用数据失败:%v", err) + return errors.New(s.t.Get("Sync app data failed: %v", err)) } if err := s.cacheRepo.UpdateRewrites(); err != nil { - return fmt.Errorf("同步伪静态规则失败:%v", err) + return errors.New(s.t.Get("Sync rewrite rules failed: %v", err)) } fmt.Println("数据同步成功") + fmt.Println(s.t.Get("Sync data successfully")) return nil } @@ -122,13 +126,13 @@ func (s *CliService) Fix(ctx context.Context, cmd *cli.Command) error { func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error { user := new(biz.User) if err := s.db.Where("id", 1).First(user).Error; err != nil { - return fmt.Errorf("获取管理员信息失败:%v", err) + return errors.New(s.t.Get("Failed to get user info: %v", err)) } password := str.Random(16) hashed, err := s.hash.Make(password) if err != nil { - return fmt.Errorf("密码生成失败:%v", err) + return errors.New(s.t.Get("Failed to generate password: %v", err)) } user.Username = str.Random(8) user.Password = hashed @@ -137,7 +141,7 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error { } if err = s.db.Save(user).Error; err != nil { - return fmt.Errorf("管理员信息保存失败:%v", err) + return errors.New(s.t.Get("Failed to save user info: %v", err)) } protocol := "http" @@ -147,39 +151,39 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error { port := s.conf.String("http.port") if port == "" { - return fmt.Errorf("端口获取失败") + return errors.New(s.t.Get("Failed to get port")) } entrance := s.conf.String("http.entrance") if entrance == "" { - return fmt.Errorf("入口获取失败") + return errors.New(s.t.Get("Failed to get entrance")) } - fmt.Printf("用户名: %s\n", user.Username) - fmt.Printf("密码: %s\n", password) - fmt.Printf("端口: %s\n", port) - fmt.Printf("入口: %s\n", entrance) + fmt.Println(s.t.Get("Username: %s", user.Username)) + fmt.Println(s.t.Get("Password: %s", password)) + fmt.Println(s.t.Get("Port: %s", port)) + fmt.Println(s.t.Get("Entrance: %s", entrance)) lv4, err := tools.GetLocalIPv4() if err == nil { - fmt.Printf("本地IPv4地址: %s://%s:%s%s\n", protocol, lv4, port, entrance) + fmt.Println(s.t.Get("Local IPv4: %s://%s:%s%s", protocol, lv4, port, entrance)) } lv6, err := tools.GetLocalIPv6() if err == nil { - fmt.Printf("本地IPv6地址: %s://[%s]:%s%s\n", protocol, lv6, port, entrance) + fmt.Println(s.t.Get("Local IPv6: %s://[%s]:%s%s", protocol, lv6, port, entrance)) } rv4, err := tools.GetPublicIPv4() if err == nil { - fmt.Printf("公网IPv4地址: %s://%s:%s%s\n", protocol, rv4, port, entrance) + fmt.Println(s.t.Get("Public IPv4: %s://%s:%s%s", protocol, rv4, port, entrance)) } rv6, err := tools.GetPublicIPv6() if err == nil { - fmt.Printf("公网IPv6地址: %s://[%s]:%s%s\n", protocol, rv6, port, entrance) + fmt.Println(s.t.Get("Public IPv6: %s://[%s]:%s%s", protocol, rv6, port, entrance)) } - fmt.Println("请根据自身网络情况自行选择合适的地址访问面板") - fmt.Printf("如无法访问,请检查服务器运营商安全组和防火墙是否放行%s端口\n", port) - fmt.Println("若仍无法访问,可尝试运行 panel-cli https off 关闭面板HTTPS") - fmt.Println("警告:关闭面板HTTPS后,面板安全性将大大降低,请谨慎操作") + fmt.Println(s.t.Get("Please choose the appropriate address to access the panel based on your network situation")) + fmt.Println(s.t.Get("If you cannot access, please check whether the server's security group and firewall allow port %s", port)) + fmt.Println(s.t.Get("If you still cannot access, try running panel-cli https off to turn off panel HTTPS")) + fmt.Println(s.t.Get("Warning: After turning off panel HTTPS, the security of the panel will be greatly reduced, please operate with caution")) return nil } @@ -187,11 +191,11 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error { func (s *CliService) UserList(ctx context.Context, cmd *cli.Command) error { users := make([]biz.User, 0) if err := s.db.Find(&users).Error; err != nil { - return fmt.Errorf("获取用户列表失败:%v", err) + return errors.New(s.t.Get("Failed to get user list: %v", err)) } for _, user := range users { - fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format(time.DateTime)) + fmt.Println(s.t.Get("ID: %d, Username: %s, Email: %s, Created At: %s", user.ID, user.Username, user.Email, user.CreatedAt.Format(time.DateTime))) } return nil @@ -202,10 +206,10 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error { oldUsername := cmd.Args().Get(0) newUsername := cmd.Args().Get(1) if oldUsername == "" { - return fmt.Errorf("旧用户名不能为空") + return errors.New(s.t.Get("Old username cannot be empty")) } if newUsername == "" { - return fmt.Errorf("新用户名不能为空") + return errors.New(s.t.Get("New username cannot be empty")) } if err := s.db.Where("username", oldUsername).First(user).Error; err != nil { diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go index 6aafffac..d845c5a2 100644 --- a/internal/service/dashboard.go +++ b/internal/service/dashboard.go @@ -11,6 +11,7 @@ import ( "github.com/go-rat/utils/collect" "github.com/hashicorp/go-version" "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" "github.com/spf13/cast" @@ -26,6 +27,7 @@ import ( ) type DashboardService struct { + t *gotext.Locale api *api.API conf *koanf.Koanf taskRepo biz.TaskRepo @@ -36,8 +38,9 @@ type DashboardService struct { backupRepo biz.BackupRepo } -func NewDashboardService(conf *koanf.Koanf, task biz.TaskRepo, website biz.WebsiteRepo, appRepo biz.AppRepo, setting biz.SettingRepo, cron biz.CronRepo, backupRepo biz.BackupRepo) *DashboardService { +func NewDashboardService(t *gotext.Locale, conf *koanf.Koanf, task biz.TaskRepo, website biz.WebsiteRepo, appRepo biz.AppRepo, setting biz.SettingRepo, cron biz.CronRepo, backupRepo biz.BackupRepo) *DashboardService { return &DashboardService{ + t: t, api: api.NewAPI(app.Version), conf: conf, taskRepo: task, @@ -52,7 +55,7 @@ func NewDashboardService(conf *koanf.Koanf, task biz.TaskRepo, website biz.Websi func (s *DashboardService) Panel(w http.ResponseWriter, r *http.Request) { name, _ := s.settingRepo.Get(biz.SettingKeyName) if name == "" { - name = "耗子面板" + name = s.t.Get("Rat Panel") } Success(w, chix.M{ @@ -64,7 +67,7 @@ func (s *DashboardService) Panel(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) HomeApps(w http.ResponseWriter, r *http.Request) { apps, err := s.appRepo.GetHomeShow() if err != nil { - Error(w, http.StatusInternalServerError, "获取首页应用失败: %v", err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to get home apps: %v", err)) return } @@ -84,7 +87,7 @@ func (s *DashboardService) Current(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) { hostInfo, err := host.Info() if err != nil { - Error(w, http.StatusInternalServerError, "获取系统信息失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to get system info: %v", err)) return } @@ -130,7 +133,7 @@ func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) { websiteCount, err := s.websiteRepo.Count() if err != nil { - Error(w, http.StatusInternalServerError, "获取网站数量失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the total number of websites: %v", err)) return } @@ -194,8 +197,8 @@ func (s *DashboardService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Requ var phpData []types.LVInt var dbData []types.LV - phpData = append(phpData, types.LVInt{Value: 0, Label: "不使用"}) - dbData = append(dbData, types.LV{Value: "0", Label: "不使用"}) + phpData = append(phpData, types.LVInt{Value: 0, Label: s.t.Get("Not used")}) + dbData = append(dbData, types.LV{Value: "0", Label: s.t.Get("Not used")}) for _, p := range php { // 过滤 phpmyadmin match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) @@ -222,25 +225,25 @@ func (s *DashboardService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Requ func (s *DashboardService) CheckUpdate(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { - Error(w, http.StatusForbidden, "离线模式下无法检查更新") + Error(w, http.StatusForbidden, s.t.Get("unable to check for updates in offline mode")) return } current := app.Version latest, err := s.api.LatestVersion() if err != nil { - Error(w, http.StatusInternalServerError, "获取最新版本失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the latest version: %v", err)) return } v1, err := version.NewVersion(current) if err != nil { - Error(w, http.StatusInternalServerError, "版本号解析失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to parse version: %v", err)) return } v2, err := version.NewVersion(latest.Version) if err != nil { - Error(w, http.StatusInternalServerError, "版本号解析失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to parse version: %v", err)) return } if v1.GreaterThanOrEqual(v2) { @@ -257,35 +260,35 @@ func (s *DashboardService) CheckUpdate(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { - Error(w, http.StatusForbidden, "离线模式下无法检查更新") + Error(w, http.StatusForbidden, s.t.Get("unable to check for updates in offline mode")) return } current := app.Version latest, err := s.api.LatestVersion() if err != nil { - Error(w, http.StatusInternalServerError, "获取最新版本失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the latest version: %v", err)) return } v1, err := version.NewVersion(current) if err != nil { - Error(w, http.StatusInternalServerError, "版本号解析失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to parse version: %v", err)) return } v2, err := version.NewVersion(latest.Version) if err != nil { - Error(w, http.StatusInternalServerError, "版本号解析失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to parse version: %v", err)) return } if v1.GreaterThanOrEqual(v2) { - Error(w, http.StatusInternalServerError, "当前版本已是最新版本") + Error(w, http.StatusInternalServerError, s.t.Get("the current version is the latest version")) return } versions, err := s.api.IntermediateVersions() if err != nil { - Error(w, http.StatusInternalServerError, "获取更新信息失败:%v", err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the update information: %v", err)) return } @@ -294,24 +297,24 @@ func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { - Error(w, http.StatusForbidden, "离线模式下无法更新") + Error(w, http.StatusForbidden, s.t.Get("unable to update in offline mode")) return } if s.taskRepo.HasRunningTask() { - Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止更新,请稍后再试") + Error(w, http.StatusInternalServerError, s.t.Get("background task is running, updating is prohibited, please try again later")) return } panel, err := s.api.LatestVersion() if err != nil { - Error(w, http.StatusInternalServerError, "获取最新版本失败:%v", err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the latest version: %v", err)) return } download := collect.First(panel.Downloads) if download == nil { - Error(w, http.StatusInternalServerError, "获取下载链接失败") + Error(w, http.StatusInternalServerError, s.t.Get("failed to get the latest version download link")) return } ver, url, checksum := panel.Version, download.URL, download.Checksum @@ -330,7 +333,7 @@ func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) { func (s *DashboardService) Restart(w http.ResponseWriter, r *http.Request) { if s.taskRepo.HasRunningTask() { - Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止重启,请稍后再试") + Error(w, http.StatusInternalServerError, s.t.Get("background task is running, restart is prohibited, please try again later")) return } diff --git a/internal/service/file.go b/internal/service/file.go index f9e49314..2da3839e 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -17,6 +17,7 @@ import ( "github.com/go-rat/chix" "github.com/go-rat/utils/file" + "github.com/leonelquinteros/gotext" "github.com/spf13/cast" "github.com/tnb-labs/panel/internal/app" @@ -29,11 +30,13 @@ import ( ) type FileService struct { + t *gotext.Locale taskRepo biz.TaskRepo } -func NewFileService(task biz.TaskRepo) *FileService { +func NewFileService(t *gotext.Locale, task biz.TaskRepo) *FileService { return &FileService{ + t: t, taskRepo: task, } } @@ -74,11 +77,11 @@ func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { return } if fileInfo.IsDir() { - Error(w, http.StatusInternalServerError, "target is a directory") + Error(w, http.StatusInternalServerError, s.t.Get("target is a directory")) return } if fileInfo.Size() > 10*1024*1024 { - Error(w, http.StatusInternalServerError, "file is too large, please download it") + Error(w, http.StatusInternalServerError, s.t.Get("file is too large, please download it to view")) return } @@ -129,7 +132,7 @@ func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) { banned := []string{"/", app.Root, filepath.Join(app.Root, "server"), filepath.Join(app.Root, "panel")} if slices.Contains(banned, req.Path) { - Error(w, http.StatusForbidden, "please don't do this") + Error(w, http.StatusForbidden, s.t.Get("please don't do this")) return } @@ -150,17 +153,17 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { path := r.FormValue("path") _, handler, err := r.FormFile("file") if err != nil { - Error(w, http.StatusInternalServerError, "upload file error: %v", err) + Error(w, http.StatusInternalServerError, s.t.Get("upload file error: %v", err)) return } if io.Exists(path) { - Error(w, http.StatusForbidden, "target path %s already exists", path) + Error(w, http.StatusForbidden, s.t.Get("target path %s already exists", path)) return } if !io.Exists(filepath.Dir(path)) { if err = stdos.MkdirAll(filepath.Dir(path), 0755); err != nil { - Error(w, http.StatusInternalServerError, "create directory error: %v", err) + Error(w, http.StatusInternalServerError, s.t.Get("create directory error: %v", err)) return } } @@ -168,12 +171,12 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { src, _ := handler.Open() out, err := stdos.OpenFile(path, stdos.O_CREATE|stdos.O_RDWR|stdos.O_TRUNC, 0644) if err != nil { - Error(w, http.StatusInternalServerError, "open file error: %v", err) + Error(w, http.StatusInternalServerError, s.t.Get("open file error: %v", err)) return } if _, err = stdio.Copy(out, src); err != nil { - Error(w, http.StatusInternalServerError, "write file error: %v", err) + Error(w, http.StatusInternalServerError, s.t.Get("write file error: %v", err)) return } @@ -216,7 +219,7 @@ func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { } if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) { - Error(w, http.StatusForbidden, "you can't do this, it will be broken") + Error(w, http.StatusForbidden, s.t.Get("you can't do this, it will be broken")) return } @@ -245,7 +248,7 @@ func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { } if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) { - Error(w, http.StatusForbidden, "you can't do this, it will be broken") + Error(w, http.StatusForbidden, s.t.Get("you can't do this, it will be broken")) return } @@ -271,7 +274,7 @@ func (s *FileService) Download(w http.ResponseWriter, r *http.Request) { return } if info.IsDir() { - Error(w, http.StatusInternalServerError, "can't download a directory") + Error(w, http.StatusInternalServerError, s.t.Get("can't download a directory")) return } @@ -289,7 +292,7 @@ func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) { timestamp := time.Now().Format("20060102150405") task := new(biz.Task) - task.Name = "下载远程文件" + task.Name = s.t.Get("Download remote file %v", filepath.Base(req.Path)) task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf(`wget -o /tmp/remote-download-%s.log -O '%s' '%s' && chmod 0755 '%s' && chown www:www '%s'`, timestamp, req.Path, req.URL, req.Path, req.Path) task.Log = fmt.Sprintf("/tmp/remote-download-%s.log", timestamp) diff --git a/internal/service/systemctl.go b/internal/service/systemctl.go index 17864fe5..fd530bb4 100644 --- a/internal/service/systemctl.go +++ b/internal/service/systemctl.go @@ -3,15 +3,20 @@ package service import ( "net/http" + "github.com/leonelquinteros/gotext" + "github.com/tnb-labs/panel/internal/http/request" "github.com/tnb-labs/panel/pkg/systemctl" ) type SystemctlService struct { + t *gotext.Locale } -func NewSystemctlService() *SystemctlService { - return &SystemctlService{} +func NewSystemctlService(t *gotext.Locale) *SystemctlService { + return &SystemctlService{ + t: t, + } } func (s *SystemctlService) Status(w http.ResponseWriter, r *http.Request) { @@ -23,7 +28,7 @@ func (s *SystemctlService) Status(w http.ResponseWriter, r *http.Request) { status, err := systemctl.Status(req.Service) if err != nil { - Error(w, http.StatusInternalServerError, "获取 %s 服务运行状态失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s service running status: %v", req.Service, err)) return } @@ -39,7 +44,7 @@ func (s *SystemctlService) IsEnabled(w http.ResponseWriter, r *http.Request) { enabled, err := systemctl.IsEnabled(req.Service) if err != nil { - Error(w, http.StatusInternalServerError, "获取 %s 服务启用状态失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s service enable status: %v", req.Service, err)) return } @@ -54,7 +59,7 @@ func (s *SystemctlService) Enable(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Enable(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "启用 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to enable %s service: %v", req.Service, err)) return } @@ -69,7 +74,7 @@ func (s *SystemctlService) Disable(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Disable(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "禁用 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to disable %s service: %v", req.Service, err)) return } @@ -84,7 +89,7 @@ func (s *SystemctlService) Restart(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Restart(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "重启 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to restart %s service: %v", req.Service, err)) return } @@ -99,7 +104,7 @@ func (s *SystemctlService) Reload(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Reload(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "重载 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to reload %s service: %v", req.Service, err)) return } @@ -114,7 +119,7 @@ func (s *SystemctlService) Start(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Start(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "启动 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to start %s service: %v", req.Service, err)) return } @@ -129,7 +134,7 @@ func (s *SystemctlService) Stop(w http.ResponseWriter, r *http.Request) { } if err = systemctl.Stop(req.Service); err != nil { - Error(w, http.StatusInternalServerError, "停止 %s 服务失败: %v", req.Service, err) + Error(w, http.StatusInternalServerError, s.t.Get("failed to stop %s service: %v", req.Service, err)) return } diff --git a/internal/service/user.go b/internal/service/user.go index b11501ea..d3ed2c1c 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -12,6 +12,7 @@ import ( "github.com/go-rat/chix" "github.com/go-rat/sessions" "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" "github.com/spf13/cast" "github.com/tnb-labs/panel/internal/biz" @@ -20,14 +21,16 @@ import ( ) type UserService struct { + t *gotext.Locale conf *koanf.Koanf session *sessions.Manager userRepo biz.UserRepo } -func NewUserService(conf *koanf.Koanf, session *sessions.Manager, user biz.UserRepo) *UserService { +func NewUserService(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager, user biz.UserRepo) *UserService { gob.Register(rsa.PrivateKey{}) // 必须注册 rsa.PrivateKey 类型否则无法反序列化 session 中的 key return &UserService{ + t: t, conf: conf, session: session, userRepo: user, @@ -72,7 +75,7 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { key, ok := sess.Get("key").(rsa.PrivateKey) if !ok { - Error(w, http.StatusForbidden, "invalid key, please refresh the page") + Error(w, http.StatusForbidden, s.t.Get("invalid key, please refresh the page")) return } diff --git a/internal/service/ws.go b/internal/service/ws.go index 257fa40b..99b655ea 100644 --- a/internal/service/ws.go +++ b/internal/service/ws.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/websocket" "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" stdssh "golang.org/x/crypto/ssh" "github.com/tnb-labs/panel/internal/biz" @@ -17,12 +18,14 @@ import ( ) type WsService struct { + t *gotext.Locale conf *koanf.Koanf sshRepo biz.SSHRepo } -func NewWsService(conf *koanf.Koanf, ssh biz.SSHRepo) *WsService { +func NewWsService(t *gotext.Locale, conf *koanf.Koanf, ssh biz.SSHRepo) *WsService { return &WsService{ + t: t, conf: conf, sshRepo: ssh, } @@ -97,14 +100,14 @@ func (s *WsService) Exec(w http.ResponseWriter, r *http.Request) { // 第一条消息是命令 _, cmd, err := ws.ReadMessage() if err != nil { - _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to read command")) + _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, s.t.Get("failed to read command: %v", err))) return } ctx, cancel := context.WithCancel(context.Background()) out, err := shell.ExecfWithPipe(ctx, string(cmd)) if err != nil { - _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to run command")) + _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, s.t.Get("failed to run command: %v", err))) cancel() return } @@ -116,7 +119,7 @@ func (s *WsService) Exec(w http.ResponseWriter, r *http.Request) { _ = ws.WriteMessage(websocket.TextMessage, []byte(line)) } if err = scanner.Err(); err != nil { - _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to read command output")) + _ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, s.t.Get("failed to read command output: %v", err))) } }()