mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 从数据库读取备份记录
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user