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-22 02:45:03 +08:00
parent 0d62d435d9
commit 3f46cb66f7
7 changed files with 82 additions and 73 deletions

View File

@@ -90,7 +90,7 @@ func initWeb() (*app.Web, error) {
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
backupService := service.NewBackupService(locale, backupRepo)
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger)
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger, settingRepo)
backupAccountService := service.NewBackupAccountService(locale, backupAccountRepo)
certService := service.NewCertService(locale, certRepo)
certDNSRepo := data.NewCertDNSRepo(db, logger)

7
go.sum
View File

@@ -164,6 +164,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -318,6 +320,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -430,6 +433,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -503,6 +508,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=

View File

@@ -4,6 +4,7 @@ import (
"context"
"log/slog"
"github.com/acepanel/panel/pkg/types"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
@@ -12,24 +13,49 @@ import (
)
type backupAccountRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
t *gotext.Locale
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
}
func NewBackupAccountRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.BackupAccountRepo {
func NewBackupAccountRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo) biz.BackupAccountRepo {
return &backupAccountRepo{
t: t,
db: db,
log: log,
t: t,
db: db,
log: log,
setting: setting,
}
}
func (r backupAccountRepo) List(page, limit uint) ([]*biz.BackupAccount, int64, error) {
accounts := make([]*biz.BackupAccount, 0)
// 本地存储
path, err := r.setting.Get(biz.SettingKeyBackupPath)
if err != nil {
return nil, 0, err
}
localStorage := &biz.BackupAccount{
ID: 0,
Type: biz.BackupAccountTypeLocal,
Name: r.t.Get("Local Storage"),
Info: types.BackupAccountInfo{
Path: path,
},
}
var dbAccounts []*biz.BackupAccount
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
if err = r.db.Model(&biz.BackupAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&dbAccounts).Error; err != nil {
return nil, 0, err
}
accounts := make([]*biz.BackupAccount, 0, len(dbAccounts)+1)
if page == 1 {
accounts = append(accounts, localStorage)
}
accounts = append(accounts, dbAccounts...)
return accounts, total + 1, nil
}
func (r backupAccountRepo) Get(id uint) (*biz.BackupAccount, error) {

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:local,s3,sftp,webdav"`
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:local,s3,sftp,webdav"`
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

@@ -5,8 +5,8 @@ export default {
list: (type: string, page: number, limit: number): any =>
http.Get(`/backup/${type}`, { params: { page, limit } }),
// 创建备份
create: (type: string, target: string, path: string): any =>
http.Post(`/backup/${type}`, { target, path }),
create: (type: string, target: string, account_id: number): any =>
http.Post(`/backup/${type}`, { target, account_id }),
// 上传备份
upload: (type: string, formData: FormData): any => http.Post(`/backup/${type}/upload`, formData),
// 删除备份

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import backupAccount from '@/api/panel/backupAccount'
import { formatDateTime } from '@/utils'
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import { formatDateTime } from '@/utils'
const { $gettext } = useGettext()
@@ -11,7 +11,6 @@ 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' }
@@ -23,7 +22,7 @@ const styleOptions = [
]
const defaultModel = {
type: 'local',
type: 's3',
name: '',
info: {
access_key: '',
@@ -79,6 +78,7 @@ const columns: any = [
width: 200,
hideInExcel: true,
render(row: any) {
const isLocal = row.type === 'local'
return [
h(
NButton,
@@ -86,6 +86,7 @@ const columns: any = [
size: 'small',
type: 'primary',
secondary: true,
disabled: isLocal,
onClick: () => handleEdit(row)
},
{
@@ -105,6 +106,7 @@ const columns: any = [
{
size: 'small',
type: 'error',
disabled: isLocal,
style: 'margin-left: 15px;'
},
{
@@ -170,9 +172,7 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="createModal = true">{{
$gettext('Add Account')
}}</n-button>
<n-button type="primary" @click="createModal = true">{{ $gettext('Add Account') }}</n-button>
</n-flex>
<n-data-table
striped
@@ -209,25 +209,12 @@ onMounted(() => {
>
<n-form :model="createModel">
<n-form-item :label="$gettext('Name')" required>
<n-input
v-model:value="createModel.name"
:placeholder="$gettext('Enter account name')"
/>
<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>
<!-- 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>
@@ -276,10 +263,7 @@ onMounted(() => {
<!-- 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-input v-model:value="createModel.info.host" :placeholder="$gettext('Enter host')" />
</n-form-item>
<n-form-item :label="$gettext('Port')" required>
<n-input-number
@@ -357,25 +341,12 @@ onMounted(() => {
>
<n-form :model="editModel">
<n-form-item :label="$gettext('Name')" required>
<n-input
v-model:value="editModel.name"
:placeholder="$gettext('Enter account name')"
/>
<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>
<!-- 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>
@@ -424,10 +395,7 @@ onMounted(() => {
<!-- 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-input v-model:value="editModel.info.host" :placeholder="$gettext('Enter host')" />
</n-form-item>
<n-form-item :label="$gettext('Port')" required>
<n-input-number
@@ -438,10 +406,7 @@ onMounted(() => {
/>
</n-form-item>
<n-form-item :label="$gettext('Username')" required>
<n-input
v-model:value="editModel.info.user"
:placeholder="$gettext('Enter username')"
/>
<n-input v-model:value="editModel.info.user" :placeholder="$gettext('Enter username')" />
</n-form-item>
<n-form-item :label="$gettext('Password')" required>
<n-input
@@ -468,10 +433,7 @@ onMounted(() => {
/>
</n-form-item>
<n-form-item :label="$gettext('Username')" required>
<n-input
v-model:value="editModel.info.user"
:placeholder="$gettext('Enter username')"
/>
<n-input v-model:value="editModel.info.user" :placeholder="$gettext('Enter username')" />
</n-form-item>
<n-form-item :label="$gettext('Password')" required>
<n-input

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import backup from '@/api/panel/backup'
import backupAccount from '@/api/panel/backupAccount'
import type { MessageReactive } from 'naive-ui'
import { NButton, NDataTable, NFlex, NInput, NPopconfirm } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
@@ -19,9 +20,11 @@ const uploadModal = ref(false)
const createModal = ref(false)
const createModel = ref({
target: '',
path: ''
account_id: 0
})
const accounts = ref<any[]>([])
const restoreModal = ref(false)
const restoreModel = ref({
file: '',
@@ -115,7 +118,7 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati
)
const handleCreate = () => {
useRequest(backup.create(type.value, createModel.value.target, createModel.value.path)).onSuccess(
useRequest(backup.create(type.value, createModel.value.target, createModel.value.account_id)).onSuccess(
() => {
createModal.value = false
window.$bus.emit('backup:refresh')
@@ -178,6 +181,15 @@ onMounted(() => {
})
}
})
useRequest(backupAccount.list(1, 10000)).onSuccess(({ data }: { data: any }) => {
for (const item of data.items) {
accounts.value.push({
label: item.name,
value: item.id
})
}
createModel.value.account_id = accounts.value[0]?.value || 0
})
refresh()
window.$bus.on('backup:refresh', refresh)
})
@@ -189,6 +201,9 @@ onUnmounted(() => {
<template>
<n-flex vertical :size="20">
<n-alert type="info">
{{ $gettext('Only local backups are displayed here. Remote backups are stored in the corresponding backup account.') }}
</n-alert>
<n-flex>
<n-button type="primary" @click="createModal = true">{{
$gettext('Create Backup')
@@ -244,12 +259,11 @@ onUnmounted(() => {
:placeholder="$gettext('Enter database name')"
/>
</n-form-item>
<n-form-item path="path" :label="$gettext('Save Directory')">
<n-input
v-model:value="createModel.path"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Leave empty to use default path')"
<n-form-item path="account_id" :label="$gettext('Backup Account')">
<n-select
v-model:value="createModel.account_id"
:options="accounts"
:placeholder="$gettext('Select backup account')"
/>
</n-form-item>
</n-form>