2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 从数据库读取备份记录

This commit is contained in:
2026-01-19 23:41:24 +08:00
parent 86ff6bb88a
commit 046105a542
5 changed files with 70 additions and 41 deletions

View File

@@ -2,8 +2,7 @@ package biz
import (
"context"
"github.com/acepanel/panel/pkg/types"
"time"
)
type BackupType string
@@ -17,8 +16,22 @@ const (
BackupTypePanel BackupType = "panel"
)
type Backup struct {
ID uint `gorm:"primaryKey" json:"id"` // 备份 ID
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的备份账号 ID
Type BackupType `gorm:"not null;default:''" json:"type"` // 备份类型
Name string `gorm:"not null;default:''" json:"name"` // 备份文件名
Size int64 `gorm:"not null;default:0" json:"size"` // 备份文件大小
Status bool `gorm:"not null;default:false" json:"status"` // 备份状态
Log string `gorm:"not null;default:''" json:"log"` // 备份日志
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Account *BackupAccount `gorm:"foreignKey:AccountID" json:"account"`
}
type BackupRepo interface {
List(typ BackupType) ([]*types.BackupFile, error)
List(page, limit uint, typ BackupType) ([]*Backup, int64, error)
Create(ctx context.Context, typ BackupType, target string, path ...string) error
Delete(ctx context.Context, typ BackupType, name string) error
Restore(ctx context.Context, typ BackupType, backup, target string) error

View File

@@ -11,9 +11,10 @@ import (
type BackupAccountType string
const (
BackupTypeS3 BackupAccountType = "s3"
BackupTypeSFTP BackupAccountType = "sftp"
BackupTypeWebDAV BackupAccountType = "webdav"
BackupAccountTypeLocal BackupAccountType = "local"
BackupAccountTypeS3 BackupAccountType = "s3"
BackupAccountTypeSFTP BackupAccountType = "sftp"
BackupAccountTypeWebDAV BackupAccountType = "webdav"
)
type BackupAccount struct {
@@ -23,6 +24,8 @@ type BackupAccount struct {
Info types.BackupAccountInfo `gorm:"not null;default:'{}';serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Backups []*Backup `gorm:"foreignKey:AccountID" json:"-"`
}
type BackupAccountRepo interface {

View File

@@ -22,7 +22,6 @@ import (
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/tools"
"github.com/acepanel/panel/pkg/types"
)
type backupRepo struct {
@@ -46,32 +45,11 @@ func NewBackupRepo(t *gotext.Locale, conf *config.Config, db *gorm.DB, log *slog
}
// List 备份列表
func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
path, err := r.GetPath(typ)
if err != nil {
return nil, err
}
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
list := make([]*types.BackupFile, 0)
for _, file := range files {
info, err := file.Info()
if err != nil {
continue
}
list = append(list, &types.BackupFile{
Name: file.Name(),
Path: filepath.Join(path, file.Name()),
Size: tools.FormatBytes(float64(info.Size())),
Time: info.ModTime(),
})
}
return list, nil
func (r *backupRepo) List(page, limit uint, typ biz.BackupType) ([]*biz.Backup, int64, error) {
backups := make([]*biz.Backup, 0)
var total int64
err := r.db.Model(&biz.Backup{}).Where("type = ?", typ).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Preload("Account").Find(&backups).Error
return backups, total, err
}
// Create 创建备份
@@ -632,19 +610,32 @@ func (r *backupRepo) FixPanel() error {
}
// 从备份目录中找最新的备份文件
list, err := r.List(biz.BackupTypePanel)
backupPath, err := r.GetPath(biz.BackupTypePanel)
if err != nil {
return err
}
slices.SortFunc(list, func(a *types.BackupFile, b *types.BackupFile) int {
return int(b.Time.Unix() - a.Time.Unix())
files, err := os.ReadDir(backupPath)
if err != nil {
return err
}
var list []os.FileInfo
for _, file := range files {
info, infoErr := file.Info()
if infoErr != nil {
continue
}
list = append(list, info)
}
slices.SortFunc(list, func(a os.FileInfo, b os.FileInfo) int {
return int(b.ModTime().Unix() - a.ModTime().Unix())
})
if len(list) == 0 {
return errors.New(r.t.Get("No backup file found, unable to automatically repair"))
}
latest := list[0]
latestPath := filepath.Join(backupPath, latest.Name())
if app.IsCli {
fmt.Println(r.t.Get("|-Backup file used: %s", latest.Name))
fmt.Println(r.t.Get("|-Backup file used: %s", latest.Name()))
}
// 解压备份文件
@@ -654,7 +645,7 @@ func (r *backupRepo) FixPanel() error {
if err = io.Remove("/tmp/panel-fix"); err != nil {
return errors.New(r.t.Get("Cleaning temporary directory failed: %v", err))
}
if err = io.UnCompress(latest.Path, "/tmp/panel-fix"); err != nil {
if err = io.UnCompress(latestPath, "/tmp/panel-fix"); err != nil {
return errors.New(r.t.Get("Unzip backup file failed: %v", err))
}

View File

@@ -3,14 +3,14 @@ package request
import "github.com/acepanel/panel/pkg/types"
type BackupAccountCreate struct {
Type string `form:"type" json:"type" validate:"required|in:s3,sftp,webdav"`
Type string `form:"type" json:"type" validate:"required|in:local,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"`
Type string `form:"type" json:"type" validate:"required|in:local,s3,sftp,webdav"`
Name string `form:"name" json:"name" validate:"required"`
Info types.BackupAccountInfo `form:"info" json:"info"`
}

View File

@@ -11,6 +11,7 @@ const editModal = ref(false)
const editId = ref(0)
const typeOptions = [
{ label: $gettext('Local'), value: 'local' },
{ label: 'S3', value: 's3' },
{ label: 'SFTP', value: 'sftp' },
{ label: 'WebDAV', value: 'webdav' }
@@ -22,7 +23,7 @@ const styleOptions = [
]
const defaultModel = {
type: 's3',
type: 'local',
name: '',
info: {
access_key: '',
@@ -56,6 +57,7 @@ const columns: any = [
width: 120,
render(row: any) {
const typeMap: Record<string, string> = {
local: $gettext('Local'),
s3: 'S3',
sftp: 'SFTP',
webdav: 'WebDAV'
@@ -216,6 +218,16 @@ onMounted(() => {
<n-select v-model:value="createModel.type" :options="typeOptions" />
</n-form-item>
<!-- Local Fields -->
<template v-if="createModel.type === 'local'">
<n-form-item :label="$gettext('Save Directory')" required>
<n-input
v-model:value="createModel.info.path"
:placeholder="$gettext('Enter save directory path')"
/>
</n-form-item>
</template>
<!-- S3 Fields -->
<template v-if="createModel.type === 's3'">
<n-form-item :label="$gettext('Access Key')" required>
@@ -354,6 +366,16 @@ onMounted(() => {
<n-select v-model:value="editModel.type" :options="typeOptions" />
</n-form-item>
<!-- Local Fields -->
<template v-if="editModel.type === 'local'">
<n-form-item :label="$gettext('Save Directory')" required>
<n-input
v-model:value="editModel.info.path"
:placeholder="$gettext('Enter save directory path')"
/>
</n-form-item>
</template>
<!-- S3 Fields -->
<template v-if="editModel.type === 's3'">
<n-form-item :label="$gettext('Access Key')" required>