mirror of https://github.com/1Panel-dev/1Panel
feat: 完成数据库备份列表
parent
8cf9c27f5f
commit
9f1e417c06
|
@ -61,6 +61,25 @@ func (b *BaseApi) DeleteBackup(c *gin.Context) {
|
|||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) DownloadRecord(c *gin.Context) {
|
||||
var req dto.DownloadRecord
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
if err := global.VALID.Struct(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := backupService.DownloadRecord(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
func (b *BaseApi) DeleteBackupRecord(c *gin.Context) {
|
||||
var req dto.BatchDeleteReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
|
|
@ -32,6 +32,12 @@ type BackupRecords struct {
|
|||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type DownloadRecord struct {
|
||||
Source string `json:"source" validate:"required,oneof=OSS S3 SFTP MINIO LOCAL"`
|
||||
FileDir string `json:"fileDir" validate:"required"`
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
}
|
||||
|
||||
type ForBuckets struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Credential string `json:"credential" validate:"required"`
|
||||
|
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
|
@ -18,6 +19,7 @@ type BackupService struct{}
|
|||
type IBackupService interface {
|
||||
List() ([]dto.BackupInfo, error)
|
||||
SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error)
|
||||
DownloadRecord(info dto.DownloadRecord) (string, error)
|
||||
Create(backupDto dto.BackupOperate) error
|
||||
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
||||
Update(id uint, upMap map[string]interface{}) error
|
||||
|
@ -62,6 +64,46 @@ func (u *BackupService) SearchRecordWithPage(search dto.BackupSearch) (int64, []
|
|||
return total, dtobas, err
|
||||
}
|
||||
|
||||
func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) {
|
||||
if info.Source == "LOCAL" {
|
||||
return info.FileDir + info.FileName, nil
|
||||
}
|
||||
backup, _ := backupRepo.Get(commonRepo.WithByType(info.Source))
|
||||
if backup.ID == 0 {
|
||||
return "", constant.ErrRecordNotFound
|
||||
}
|
||||
varMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
|
||||
return "", err
|
||||
}
|
||||
varMap["type"] = backup.Type
|
||||
varMap["bucket"] = backup.Bucket
|
||||
switch backup.Type {
|
||||
case constant.Sftp:
|
||||
varMap["password"] = backup.Credential
|
||||
case constant.OSS, constant.S3, constant.MinIo:
|
||||
varMap["secretKey"] = backup.Credential
|
||||
}
|
||||
backClient, err := cloud_storage.NewCloudStorageClient(varMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new cloud storage client failed, err: %v", err)
|
||||
}
|
||||
tempPath := fmt.Sprintf("%s%s", constant.DownloadDir, info.FileDir)
|
||||
if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(tempPath, os.ModePerm); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
targetPath := tempPath + info.FileName
|
||||
if _, err = os.Stat(targetPath); err != nil && os.IsNotExist(err) {
|
||||
isOK, err := backClient.Download(info.FileName, targetPath)
|
||||
if !isOK {
|
||||
return "", fmt.Errorf("cloud storage download failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func (u *BackupService) Create(backupDto dto.BackupOperate) error {
|
||||
backup, _ := backupRepo.Get(commonRepo.WithByType(backupDto.Type))
|
||||
if backup.ID != 0 {
|
||||
|
|
|
@ -25,6 +25,7 @@ func (s *BackupRouter) InitBackupRouter(Router *gin.RouterGroup) {
|
|||
baRouter.POST("/buckets", baseApi.ListBuckets)
|
||||
withRecordRouter.POST("", baseApi.CreateBackup)
|
||||
withRecordRouter.POST("/del", baseApi.DeleteBackup)
|
||||
withRecordRouter.POST("/record/download", baseApi.DownloadRecord)
|
||||
withRecordRouter.POST("/record/del", baseApi.DeleteBackupRecord)
|
||||
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,11 @@ export namespace Backup {
|
|||
credential: string;
|
||||
vars: string;
|
||||
}
|
||||
export interface RecordDownload {
|
||||
source: string;
|
||||
fileDir: string;
|
||||
fileName: string;
|
||||
}
|
||||
export interface RecordInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
|
|
|
@ -16,6 +16,10 @@ export const editBackup = (params: Backup.BackupOperate) => {
|
|||
export const deleteBackup = (params: { ids: number[] }) => {
|
||||
return http.post(`/backups/del`, params);
|
||||
};
|
||||
|
||||
export const downloadBackupRecord = (params: Backup.RecordDownload) => {
|
||||
return http.download<BlobPart>(`/backups/record/download`, params, { responseType: 'blob' });
|
||||
};
|
||||
export const deleteBackupRecord = (params: { ids: number[] }) => {
|
||||
return http.post(`/backups/record/del`, params);
|
||||
};
|
||||
|
|
|
@ -27,6 +27,8 @@ export default {
|
|||
expand: '展开',
|
||||
log: '日志',
|
||||
back: '返回',
|
||||
recover: '恢复',
|
||||
download: '下载',
|
||||
saveAndEnable: '保存并启用',
|
||||
},
|
||||
search: {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
@ -36,10 +36,10 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
|||
import { reactive, ref } from 'vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { backup, searchBackupRecords } from '@/api/modules/database';
|
||||
import { backup, recover, searchBackupRecords } from '@/api/modules/database';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { deleteBackupRecord } from '@/api/modules/backup';
|
||||
import { deleteBackupRecord, downloadBackupRecord } from '@/api/modules/backup';
|
||||
import { Backup } from '@/api/interface/backup';
|
||||
|
||||
const selects = ref<any>([]);
|
||||
|
@ -87,6 +87,32 @@ const onBackup = async () => {
|
|||
search();
|
||||
};
|
||||
|
||||
const onRecover = async (row: Backup.RecordInfo) => {
|
||||
let params = {
|
||||
version: version.value,
|
||||
dbName: dbName.value,
|
||||
backupName: row.fileDir + row.fileName,
|
||||
};
|
||||
await recover(params);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
const onDownload = async (row: Backup.RecordInfo) => {
|
||||
let params = {
|
||||
source: row.source,
|
||||
fileDir: row.fileDir,
|
||||
fileName: row.fileName,
|
||||
};
|
||||
const res = await downloadBackupRecord(params);
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = row.fileName;
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
||||
let ids: Array<number> = [];
|
||||
if (row) {
|
||||
|
@ -103,11 +129,22 @@ const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
|||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
icon: 'Delete',
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onBatchDelete(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onRecover(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.download'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onDownload(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
defineExpose({
|
||||
|
|
Loading…
Reference in New Issue