feat: Support user-defined description for backup (#8354)

Refs #4735
This commit is contained in:
ssongliu 2025-04-09 10:15:54 +08:00 committed by GitHub
parent d67dfe2dfd
commit 0934534ac5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 89 additions and 62 deletions

View file

@ -303,6 +303,27 @@ func (b *BaseApi) DownloadRecord(c *gin.Context) {
helper.SuccessWithData(c, filePath)
}
// @Tags Backup Account
// @Summary Update backup record description
// @Accept json
// @Param request body dto.UpdateDescription true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /backup/record/description/update [post]
func (b *BaseApi) UpdateRecordDescription(c *gin.Context) {
var req dto.UpdateDescription
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := backupRecordService.UpdateDescription(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Backup Account
// @Summary Delete backup record
// @Accept json

View file

@ -60,6 +60,8 @@ type CommonBackup struct {
Secret string `json:"secret"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Description string `json:"description"`
}
type CommonRecover struct {
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
@ -92,6 +94,7 @@ type BackupRecords struct {
DownloadAccountID uint `json:"downloadAccountID"`
FileDir string `json:"fileDir"`
FileName string `json:"fileName"`
Description string `json:"description"`
}
type DownloadRecord struct {

View file

@ -26,4 +26,6 @@ type BackupRecord struct {
DetailName string `json:"detailName"`
FileDir string `json:"fileDir"`
FileName string `json:"fileName"`
Description string `json:"description"`
}

View file

@ -24,6 +24,7 @@ type IBackupRepo interface {
CreateRecord(record *model.BackupRecord) error
DeleteRecord(ctx context.Context, opts ...DBOption) error
UpdateRecord(record *model.BackupRecord) error
UpdateRecordByMap(id uint, upMap map[string]interface{}) error
WithByDetailName(detailName string) DBOption
WithByFileName(fileName string) DBOption
WithByCronID(cronjobID uint) DBOption
@ -144,6 +145,10 @@ func (u *BackupRepo) UpdateRecord(record *model.BackupRecord) error {
return global.DB.Save(record).Error
}
func (u *BackupRepo) UpdateRecordByMap(id uint, upMap map[string]interface{}) error {
return global.DB.Model(&model.BackupRecord{}).Where("id = ?", id).Updates(upMap).Error
}
func (u *BackupRepo) DeleteRecord(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.BackupRecord{}).Error
}

View file

@ -58,6 +58,7 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
Description: req.Description,
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)

View file

@ -43,6 +43,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
Description: req.Description,
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)

View file

@ -44,6 +44,7 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
Description: req.Description,
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)

View file

@ -26,6 +26,7 @@ type IBackupRecordService interface {
ListFiles(req dto.OperateByID) []string
LoadRecordSize(req dto.SearchForSize) ([]dto.RecordFileSize, error)
UpdateDescription(req dto.UpdateDescription) error
}
func NewIBackupRecordService() IBackupRecordService {
@ -261,3 +262,7 @@ func (u *BackupRecordService) LoadRecordSize(req dto.SearchForSize) ([]dto.Recor
wg.Wait()
return datas, nil
}
func (u *BackupRecordService) UpdateDescription(req dto.UpdateDescription) error {
return backupRepo.UpdateRecordByMap(req.ID, map[string]interface{}{"description": req.Description})
}

View file

@ -54,6 +54,7 @@ func (u *BackupService) RedisBackup(req dto.CommonBackup) error {
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
Description: req.Description,
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)

View file

@ -51,6 +51,7 @@ func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error {
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
Description: req.Description,
}
if err = backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)

View file

@ -741,31 +741,3 @@ func scanFile(pathItem string, size *int64, count *int) {
}
}
}
func loadRestorePath(upgradeDir string) (string, error) {
if _, err := os.Stat(upgradeDir); err != nil && os.IsNotExist(err) {
return "no such file", nil
}
files, err := os.ReadDir(upgradeDir)
if err != nil {
return "", err
}
type itemState struct {
Name string
CreateAt time.Time
}
var folders []itemState
for _, file := range files {
if file.IsDir() {
info, _ := file.Info()
folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
}
}
if len(folders) == 0 {
return "no such file", nil
}
sort.Slice(folders, func(i, j int) bool {
return folders[i].CreateAt.After(folders[j].CreateAt)
})
return folders[0].Name, nil
}

View file

@ -19,7 +19,7 @@ import (
)
var AddTable = &gormigrate.Migration{
ID: "20250108-add-table",
ID: "20250408-add-table",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.AppDetail{},

View file

@ -32,5 +32,6 @@ func (s *BackupRouter) InitRouter(Router *gin.RouterGroup) {
backupRouter.POST("/record/search/bycronjob", baseApi.SearchBackupRecordsByCronjob)
backupRouter.POST("/record/download", baseApi.DownloadRecord)
backupRouter.POST("/record/del", baseApi.DeleteBackupRecord)
backupRouter.POST("/record/description/update", baseApi.UpdateRecordDescription)
}
}

View file

@ -32,6 +32,9 @@ export const downloadBackupRecord = (params: Backup.RecordDownload) => {
export const deleteBackupRecord = (params: { ids: number[] }) => {
return http.post(`/backups/record/del`, params);
};
export const updateRecordDescription = (id: Number, description: String) => {
return http.post(`/backups/record/description/update`, { id: id, description: description });
};
export const searchBackupRecords = (params: Backup.SearchBackupRecord) => {
return http.post<ResPage<Backup.RecordInfo>>(`/backups/record/search`, params, TimeoutEnum.T_5M);
};

View file

@ -4,27 +4,27 @@
:header="$t('commons.button.backup')"
:resource="detailName ? name + ' [' + detailName + ']' : name"
@close="handleClose"
size="large"
size="60%"
>
<template #content>
<div class="mb-5" v-if="type === 'app'">
<el-alert :closable="false" type="warning">
<div class="mt-2 text-xs">
<span>{{ $t('setting.backupJump') }}</span>
<span class="jump" @click="goFile()">
<el-icon class="ml-2"><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</span>
</div>
</el-alert>
</div>
<el-alert v-if="type === 'app'" :closable="false" type="warning">
<div class="mt-2 text-xs">
<span>{{ $t('setting.backupJump') }}</span>
<span class="jump" @click="goFile()">
<el-icon class="ml-2"><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</span>
</div>
</el-alert>
<ComplexTable
class="mt-5"
v-loading="loading"
:pagination-config="paginationConfig"
v-model:selects="selects"
@search="search"
:data="data"
style="width: 100%"
>
<template #toolbar>
<el-button type="primary" :disabled="status && status != 'Running'" @click="onBackup()">
@ -49,7 +49,7 @@
</div>
</template>
</el-table-column>
<el-table-column :label="$t('app.source')" prop="backupType">
<el-table-column min-width="100px" :label="$t('app.source')" prop="backupType">
<template #default="{ row }">
<span v-if="row.accountType === 'LOCAL'">
{{ $t('setting.LOCAL') }}
@ -61,13 +61,24 @@
</template>
</el-table-column>
<el-table-column
min-width="120px"
:label="$t('commons.table.description')"
prop="description"
show-overflow-tooltip
>
<template #default="{ row }">
<fu-input-rw-switch v-model="row.description" @blur="onChange(row)" />
</template>
</el-table-column>
<el-table-column
min-width="80px"
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
/>
<fu-table-operations width="230px" :buttons="buttons" :label="$t('commons.table.operate')" fix />
<fu-table-operations width="200px" :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</template>
</DrawerPro>
@ -78,14 +89,16 @@
size="small"
@close="handleBackupClose"
>
<el-form ref="backupForm" @submit.prevent label-position="left" v-loading="loading">
<el-form-item
:label="$t('setting.compressPassword')"
class="mt-10"
v-if="type === 'app' || type === 'website'"
>
<el-alert :closable="false">
{{ $t('commons.msg.' + (isBackup ? 'backupHelper' : 'recoverHelper'), [name + '( ' + detailName + ' )']) }}
</el-alert>
<el-form class="mt-5" ref="backupForm" @submit.prevent label-position="top" v-loading="loading">
<el-form-item :label="$t('setting.compressPassword')" v-if="type === 'app' || type === 'website'">
<el-input v-model="secret" :placeholder="$t('setting.backupRecoverMessage')" />
</el-form-item>
<el-form-item v-if="isBackup" :label="$t('commons.table.description')">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" v-model="description" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -114,6 +127,7 @@ import {
downloadBackupRecord,
searchBackupRecords,
loadRecordSize,
updateRecordDescription,
} from '@/api/modules/backup';
import i18n from '@/lang';
import { Backup } from '@/api/interface/backup';
@ -142,6 +156,7 @@ const detailName = ref();
const backupPath = ref();
const status = ref();
const secret = ref();
const description = ref();
const open = ref();
const isBackup = ref();
@ -181,6 +196,11 @@ const goFile = async () => {
router.push({ name: 'File', query: { path: `${backupPath.value}/app/${name.value}/${detailName.value}` } });
};
const onChange = async (info: any) => {
await updateRecordDescription(info.id, info.description);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const search = async () => {
let params = {
page: paginationConfig.currentPage,
@ -236,6 +256,7 @@ const backup = async (close: boolean) => {
detailName: detailName.value,
secret: secret.value,
taskID: taskID,
description: description.value,
};
loading.value = true;
try {
@ -285,20 +306,8 @@ const recover = async (close: boolean, row?: any) => {
};
const onBackup = async () => {
description.value = '';
isBackup.value = true;
if (type.value !== 'app' && type.value !== 'website') {
ElMessageBox.confirm(
i18n.global.t('commons.msg.backupHelper', [name.value + '( ' + detailName.value + ' )']),
i18n.global.t('commons.button.backup'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
).then(async () => {
backup(true);
});
return;
}
open.value = true;
};

View file

@ -103,6 +103,7 @@ onMounted(() => {
let itemSize = Number(localStorage.getItem(props.paginationConfig.cacheSizeKey));
if (itemSize) {
props.paginationConfig.pageSize = itemSize;
sizeChange();
}
}
let heightDiff = 320;