mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 09:13:49 +08:00
feat: 备份账号设置
This commit is contained in:
@@ -90,6 +90,8 @@ func initWeb() (*app.Web, error) {
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger)
|
||||
backupAccountService := service.NewBackupAccountService(backupAccountRepo)
|
||||
certService := service.NewCertService(locale, certRepo)
|
||||
certDNSRepo := data.NewCertDNSRepo(db, logger)
|
||||
certDNSService := service.NewCertDNSService(certDNSRepo)
|
||||
@@ -156,7 +158,7 @@ func initWeb() (*app.Web, error) {
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, templateService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, backupAccountService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, templateService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
6
go.mod
6
go.mod
@@ -80,13 +80,13 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/libtnb/securecookie v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
@@ -95,7 +95,7 @@ require (
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -116,7 +116,6 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
@@ -236,8 +235,8 @@ github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696 h1:GN0Y3DG27mMruX53
|
||||
github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -295,8 +294,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
@@ -371,8 +370,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -460,7 +459,6 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
|
||||
34
internal/biz/backup_account.go
Normal file
34
internal/biz/backup_account.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
)
|
||||
|
||||
type BackupAccountType string
|
||||
|
||||
const (
|
||||
BackupTypeS3 BackupAccountType = "s3"
|
||||
BackupTypeSFTP BackupAccountType = "sftp"
|
||||
BackupTypeWebDAV BackupAccountType = "webdav"
|
||||
)
|
||||
|
||||
type BackupAccount struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Type BackupAccountType `gorm:"not null;default:''" json:"type"`
|
||||
Name string `gorm:"not null;default:''" json:"name"`
|
||||
Info types.BackupAccountInfo `gorm:"not null;default:'{}';serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BackupAccountRepo interface {
|
||||
List(page, limit uint) ([]*BackupAccount, int64, error)
|
||||
Get(id uint) (*BackupAccount, error)
|
||||
Create(ctx context.Context, req *request.BackupAccountCreate) (*BackupAccount, error)
|
||||
Update(ctx context.Context, req *request.BackupAccountUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type CertDNS struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:''" json:"name"` // 备注名称
|
||||
Type acme.DnsType `gorm:"not null;default:'aliyun'" json:"type"` // DNS 提供商
|
||||
Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"`
|
||||
Data acme.DNSParam `gorm:"not null;default:'{}';serializer:json" json:"dns_param"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
|
||||
84
internal/data/backup_account.go
Normal file
84
internal/data/backup_account.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type backupAccountRepo struct {
|
||||
t *gotext.Locale
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewBackupAccountRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.BackupAccountRepo {
|
||||
return &backupAccountRepo{
|
||||
t: t,
|
||||
db: db,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (r backupAccountRepo) List(page, limit uint) ([]*biz.BackupAccount, int64, error) {
|
||||
accounts := make([]*biz.BackupAccount, 0)
|
||||
var total int64
|
||||
err := r.db.Model(&biz.BackupAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&accounts).Error
|
||||
return accounts, total, err
|
||||
}
|
||||
|
||||
func (r backupAccountRepo) Get(id uint) (*biz.BackupAccount, error) {
|
||||
account := new(biz.BackupAccount)
|
||||
err := r.db.Model(&biz.BackupAccount{}).Where("id = ?", id).First(account).Error
|
||||
return account, err
|
||||
}
|
||||
|
||||
func (r backupAccountRepo) Create(ctx context.Context, req *request.BackupAccountCreate) (*biz.BackupAccount, error) {
|
||||
account := &biz.BackupAccount{
|
||||
Type: biz.BackupAccountType(req.Type),
|
||||
Name: req.Name,
|
||||
Info: req.Info,
|
||||
}
|
||||
|
||||
if err := r.db.Create(account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.log.Info("backup account created", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(account.ID)), slog.String("account_type", req.Type), slog.String("name", req.Name))
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r backupAccountRepo) Update(ctx context.Context, req *request.BackupAccountUpdate) error {
|
||||
account, err := r.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.Type = biz.BackupAccountType(req.Type)
|
||||
account.Name = req.Name
|
||||
account.Info = req.Info
|
||||
|
||||
if err = r.db.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Info("backup account updated", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("account_type", req.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r backupAccountRepo) Delete(ctx context.Context, id uint) error {
|
||||
if err := r.db.Model(&biz.BackupAccount{}).Where("id = ?", id).Delete(&biz.BackupAccount{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Info("backup account deleted", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import "github.com/google/wire"
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewAppRepo,
|
||||
NewBackupRepo,
|
||||
NewBackupAccountRepo,
|
||||
NewCacheRepo,
|
||||
NewCertRepo,
|
||||
NewCertAccountRepo,
|
||||
|
||||
16
internal/http/request/backup_account.go
Normal file
16
internal/http/request/backup_account.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package request
|
||||
|
||||
import "github.com/acepanel/panel/pkg/types"
|
||||
|
||||
type BackupAccountCreate struct {
|
||||
Type string `form:"type" json:"type" validate:"required|in:s3,sftp,webdav"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Info types.BackupAccountInfo `form:"info" json:"info"`
|
||||
}
|
||||
|
||||
type BackupAccountUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:backup_accounts,id"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:s3,sftp,webdav"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Info types.BackupAccountInfo `form:"info" json:"info"`
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type Http struct {
|
||||
databaseServer *service.DatabaseServerService
|
||||
databaseUser *service.DatabaseUserService
|
||||
backup *service.BackupService
|
||||
backupAccount *service.BackupAccountService
|
||||
cert *service.CertService
|
||||
certDNS *service.CertDNSService
|
||||
certAccount *service.CertAccountService
|
||||
@@ -74,6 +75,7 @@ func NewHttp(
|
||||
databaseServer *service.DatabaseServerService,
|
||||
databaseUser *service.DatabaseUserService,
|
||||
backup *service.BackupService,
|
||||
backupAccount *service.BackupAccountService,
|
||||
cert *service.CertService,
|
||||
certDNS *service.CertDNSService,
|
||||
certAccount *service.CertAccountService,
|
||||
@@ -120,6 +122,7 @@ func NewHttp(
|
||||
databaseServer: databaseServer,
|
||||
databaseUser: databaseUser,
|
||||
backup: backup,
|
||||
backupAccount: backupAccount,
|
||||
cert: cert,
|
||||
certDNS: certDNS,
|
||||
certAccount: certAccount,
|
||||
@@ -265,6 +268,14 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/{type}/restore", route.backup.Restore)
|
||||
})
|
||||
|
||||
r.Route("/backup_account", func(r chi.Router) {
|
||||
r.Get("/", route.backupAccount.List)
|
||||
r.Post("/", route.backupAccount.Create)
|
||||
r.Put("/{id}", route.backupAccount.Update)
|
||||
r.Get("/{id}", route.backupAccount.Get)
|
||||
r.Delete("/{id}", route.backupAccount.Delete)
|
||||
})
|
||||
|
||||
r.Route("/cert", func(r chi.Router) {
|
||||
r.Get("/ca_providers", route.cert.CAProviders)
|
||||
r.Get("/dns_providers", route.cert.DNSProviders)
|
||||
|
||||
101
internal/service/backup_account.go
Normal file
101
internal/service/backup_account.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type BackupAccountService struct {
|
||||
backupAccountRepo biz.BackupAccountRepo
|
||||
}
|
||||
|
||||
func NewBackupAccountService(backupAccount biz.BackupAccountRepo) *BackupAccountService {
|
||||
return &BackupAccountService{
|
||||
backupAccountRepo: backupAccount,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BackupAccountService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
accounts, total, err := s.backupAccountRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": accounts,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BackupAccountService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupAccountCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
account, err := s.backupAccountRepo.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, account)
|
||||
}
|
||||
|
||||
func (s *BackupAccountService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupAccountUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupAccountRepo.Update(r.Context(), req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *BackupAccountService) Get(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
account, err := s.backupAccountRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, account)
|
||||
}
|
||||
|
||||
func (s *BackupAccountService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupAccountRepo.Delete(r.Context(), req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import "github.com/google/wire"
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewAppService,
|
||||
NewBackupService,
|
||||
NewBackupAccountService,
|
||||
NewCertService,
|
||||
NewCertAccountService,
|
||||
NewCertDNSService,
|
||||
|
||||
@@ -2,6 +2,24 @@ package types
|
||||
|
||||
import "time"
|
||||
|
||||
type BackupAccountInfo struct {
|
||||
// S3
|
||||
AccessKey string `json:"access_key"` // 访问密钥
|
||||
SecretKey string `json:"secret_key"` // 私钥
|
||||
Style string `json:"style"` // virtual_hosted, path
|
||||
Region string `json:"region"` // 地区
|
||||
Endpoint string `json:"endpoint"` // 端点
|
||||
Bucket string `json:"bucket"` // 存储桶
|
||||
|
||||
// SFTP / WebDAV
|
||||
Host string `json:"host"` // 主机
|
||||
Port int `json:"port"` // 端口
|
||||
User string `json:"user"` // 用户名
|
||||
Password string `json:"password"` // 密码
|
||||
|
||||
Path string `json:"path"` // 路径
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
|
||||
15
web/src/api/panel/backupAccount/index.ts
Normal file
15
web/src/api/panel/backupAccount/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取备份账号列表
|
||||
list: (page: number, limit: number): any =>
|
||||
http.Get('/backup_account', { params: { page, limit } }),
|
||||
// 获取备份账号
|
||||
get: (id: number): any => http.Get(`/backup_account/${id}`),
|
||||
// 创建备份账号
|
||||
create: (data: any): any => http.Post('/backup_account', data),
|
||||
// 更新备份账号
|
||||
update: (id: number, data: any): any => http.Put(`/backup_account/${id}`, data),
|
||||
// 删除备份账号
|
||||
delete: (id: number): any => http.Delete(`/backup_account/${id}`)
|
||||
}
|
||||
474
web/src/views/backup/AccountView.vue
Normal file
474
web/src/views/backup/AccountView.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<script setup lang="ts">
|
||||
import backupAccount from '@/api/panel/backupAccount'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const createModal = ref(false)
|
||||
const editModal = ref(false)
|
||||
const editId = ref(0)
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'S3', value: 's3' },
|
||||
{ label: 'SFTP', value: 'sftp' },
|
||||
{ label: 'WebDAV', value: 'webdav' }
|
||||
]
|
||||
|
||||
const styleOptions = [
|
||||
{ label: 'Virtual Hosted', value: 'virtual_hosted' },
|
||||
{ label: 'Path', value: 'path' }
|
||||
]
|
||||
|
||||
const defaultModel = {
|
||||
type: 's3',
|
||||
name: '',
|
||||
info: {
|
||||
access_key: '',
|
||||
secret_key: '',
|
||||
style: 'virtual_hosted',
|
||||
region: '',
|
||||
endpoint: '',
|
||||
bucket: '',
|
||||
host: '',
|
||||
port: 22,
|
||||
user: '',
|
||||
password: '',
|
||||
path: ''
|
||||
}
|
||||
}
|
||||
|
||||
const createModel = ref({ ...defaultModel, info: { ...defaultModel.info } })
|
||||
const editModel = ref({ ...defaultModel, info: { ...defaultModel.info } })
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'name',
|
||||
minWidth: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Type'),
|
||||
key: 'type',
|
||||
width: 120,
|
||||
render(row: any) {
|
||||
const typeMap: Record<string, string> = {
|
||||
s3: 'S3',
|
||||
sftp: 'SFTP',
|
||||
webdav: 'WebDAV'
|
||||
}
|
||||
return typeMap[row.type] || row.type
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Created At'),
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render(row: any) {
|
||||
return formatDateTime(row.created_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleEdit(row)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Edit')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Are you sure you want to delete this account?'),
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Delete')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => backupAccount.list(page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 20,
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items
|
||||
}
|
||||
)
|
||||
|
||||
const handleCreate = () => {
|
||||
useRequest(backupAccount.create(createModel.value)).onSuccess(() => {
|
||||
createModal.value = false
|
||||
createModel.value = { ...defaultModel, info: { ...defaultModel.info } }
|
||||
refresh()
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
editId.value = row.id
|
||||
editModel.value = {
|
||||
type: row.type,
|
||||
name: row.name,
|
||||
info: { ...defaultModel.info, ...row.info }
|
||||
}
|
||||
editModal.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
useRequest(backupAccount.update(editId.value, editModel.value)).onSuccess(() => {
|
||||
editModal.value = false
|
||||
refresh()
|
||||
window.$message.success($gettext('Updated successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
useRequest(backupAccount.delete(id)).onSuccess(() => {
|
||||
refresh()
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical :size="20">
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="createModal = true">{{
|
||||
$gettext('Add Account')
|
||||
}}</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="800"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => row.id"
|
||||
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>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<n-modal
|
||||
v-model:show="createModal"
|
||||
preset="card"
|
||||
:title="$gettext('Add Account')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="createModal = false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item :label="$gettext('Name')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.name"
|
||||
:placeholder="$gettext('Enter account name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Type')" required>
|
||||
<n-select v-model:value="createModel.type" :options="typeOptions" />
|
||||
</n-form-item>
|
||||
|
||||
<!-- S3 Fields -->
|
||||
<template v-if="createModel.type === 's3'">
|
||||
<n-form-item :label="$gettext('Access Key')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.access_key"
|
||||
:placeholder="$gettext('Enter access key')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Secret Key')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.secret_key"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter secret key')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Style')">
|
||||
<n-select v-model:value="createModel.info.style" :options="styleOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Region')">
|
||||
<n-input
|
||||
v-model:value="createModel.info.region"
|
||||
:placeholder="$gettext('Enter region (e.g., us-east-1)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Endpoint')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.endpoint"
|
||||
:placeholder="$gettext('Enter endpoint URL')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Bucket')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.bucket"
|
||||
:placeholder="$gettext('Enter bucket name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')">
|
||||
<n-input
|
||||
v-model:value="createModel.info.path"
|
||||
:placeholder="$gettext('Enter path (optional)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<!-- SFTP Fields -->
|
||||
<template v-if="createModel.type === 'sftp'">
|
||||
<n-form-item :label="$gettext('Host')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.host"
|
||||
:placeholder="$gettext('Enter host')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Port')" required>
|
||||
<n-input-number
|
||||
v-model:value="createModel.info.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
:placeholder="$gettext('Enter port')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Username')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.user"
|
||||
:placeholder="$gettext('Enter username')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter password')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.path"
|
||||
:placeholder="$gettext('Enter remote path')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<!-- WebDAV Fields -->
|
||||
<template v-if="createModel.type === 'webdav'">
|
||||
<n-form-item :label="$gettext('Host')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.host"
|
||||
:placeholder="$gettext('Enter WebDAV URL')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Username')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.user"
|
||||
:placeholder="$gettext('Enter username')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.info.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter password')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')">
|
||||
<n-input
|
||||
v-model:value="createModel.info.path"
|
||||
:placeholder="$gettext('Enter path (optional)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleCreate">{{ $gettext('Submit') }}</n-button>
|
||||
</n-modal>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<n-modal
|
||||
v-model:show="editModal"
|
||||
preset="card"
|
||||
:title="$gettext('Edit Account')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="editModal = false"
|
||||
>
|
||||
<n-form :model="editModel">
|
||||
<n-form-item :label="$gettext('Name')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.name"
|
||||
:placeholder="$gettext('Enter account name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Type')" required>
|
||||
<n-select v-model:value="editModel.type" :options="typeOptions" />
|
||||
</n-form-item>
|
||||
|
||||
<!-- S3 Fields -->
|
||||
<template v-if="editModel.type === 's3'">
|
||||
<n-form-item :label="$gettext('Access Key')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.access_key"
|
||||
:placeholder="$gettext('Enter access key')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Secret Key')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.secret_key"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter secret key')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Style')">
|
||||
<n-select v-model:value="editModel.info.style" :options="styleOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Region')">
|
||||
<n-input
|
||||
v-model:value="editModel.info.region"
|
||||
:placeholder="$gettext('Enter region (e.g., us-east-1)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Endpoint')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.endpoint"
|
||||
:placeholder="$gettext('Enter endpoint URL')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Bucket')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.bucket"
|
||||
:placeholder="$gettext('Enter bucket name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')">
|
||||
<n-input
|
||||
v-model:value="editModel.info.path"
|
||||
:placeholder="$gettext('Enter path (optional)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<!-- SFTP Fields -->
|
||||
<template v-if="editModel.type === 'sftp'">
|
||||
<n-form-item :label="$gettext('Host')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.host"
|
||||
:placeholder="$gettext('Enter host')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Port')" required>
|
||||
<n-input-number
|
||||
v-model:value="editModel.info.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
:placeholder="$gettext('Enter port')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Username')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.user"
|
||||
:placeholder="$gettext('Enter username')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter password')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.path"
|
||||
:placeholder="$gettext('Enter remote path')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<!-- WebDAV Fields -->
|
||||
<template v-if="editModel.type === 'webdav'">
|
||||
<n-form-item :label="$gettext('Host')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.host"
|
||||
:placeholder="$gettext('Enter WebDAV URL')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Username')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.user"
|
||||
:placeholder="$gettext('Enter username')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password')" required>
|
||||
<n-input
|
||||
v-model:value="editModel.info.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter password')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Path')">
|
||||
<n-input
|
||||
v-model:value="editModel.info.path"
|
||||
:placeholder="$gettext('Enter path (optional)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleUpdate">{{ $gettext('Submit') }}</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -5,6 +5,7 @@ defineOptions({
|
||||
|
||||
import home from '@/api/panel/home'
|
||||
import ListView from '@/views/backup/ListView.vue'
|
||||
import AccountView from '@/views/backup/AccountView.vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
@@ -37,8 +38,10 @@ const postgreSQLInstalled = computed(() => {
|
||||
<n-tab name="website" :tab="$gettext('Website')" />
|
||||
<n-tab v-if="mySQLInstalled" name="mysql" tab="MySQL" />
|
||||
<n-tab v-if="postgreSQLInstalled" name="postgres" tab="PostgreSQL" />
|
||||
<n-tab name="account" :tab="$gettext('Account')" />
|
||||
</n-tabs>
|
||||
</template>
|
||||
<list-view v-model:type="currentTab" />
|
||||
<list-view v-if="currentTab !== 'account'" v-model:type="currentTab" />
|
||||
<account-view v-else />
|
||||
</common-page>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user