2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 09:13:49 +08:00

feat: 备份账号设置

This commit is contained in:
2026-01-19 23:05:20 +08:00
parent 30f94c920c
commit ddd19f1597
15 changed files with 772 additions and 14 deletions

View File

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

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

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

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

View File

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

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

View File

@@ -6,6 +6,7 @@ import "github.com/google/wire"
var ProviderSet = wire.NewSet(
NewAppRepo,
NewBackupRepo,
NewBackupAccountRepo,
NewCacheRepo,
NewCertRepo,
NewCertAccountRepo,

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

View File

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

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

View File

@@ -6,6 +6,7 @@ import "github.com/google/wire"
var ProviderSet = wire.NewSet(
NewAppService,
NewBackupService,
NewBackupAccountService,
NewCertService,
NewCertAccountService,
NewCertDNSService,

View File

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

View 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}`)
}

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

View File

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