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)
|
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) {
|
func (b *BaseApi) DeleteBackupRecord(c *gin.Context) {
|
||||||
var req dto.BatchDeleteReq
|
var req dto.BatchDeleteReq
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|
|
@ -32,6 +32,12 @@ type BackupRecords struct {
|
||||||
FileName string `json:"fileName"`
|
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 ForBuckets struct {
|
||||||
Type string `json:"type" validate:"required"`
|
Type string `json:"type" validate:"required"`
|
||||||
Credential string `json:"credential" validate:"required"`
|
Credential string `json:"credential" validate:"required"`
|
||||||
|
|
|
@ -2,6 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
@ -18,6 +19,7 @@ type BackupService struct{}
|
||||||
type IBackupService interface {
|
type IBackupService interface {
|
||||||
List() ([]dto.BackupInfo, error)
|
List() ([]dto.BackupInfo, error)
|
||||||
SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error)
|
SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error)
|
||||||
|
DownloadRecord(info dto.DownloadRecord) (string, error)
|
||||||
Create(backupDto dto.BackupOperate) error
|
Create(backupDto dto.BackupOperate) error
|
||||||
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
||||||
Update(id uint, upMap map[string]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
|
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 {
|
func (u *BackupService) Create(backupDto dto.BackupOperate) error {
|
||||||
backup, _ := backupRepo.Get(commonRepo.WithByType(backupDto.Type))
|
backup, _ := backupRepo.Get(commonRepo.WithByType(backupDto.Type))
|
||||||
if backup.ID != 0 {
|
if backup.ID != 0 {
|
||||||
|
|
|
@ -25,6 +25,7 @@ func (s *BackupRouter) InitBackupRouter(Router *gin.RouterGroup) {
|
||||||
baRouter.POST("/buckets", baseApi.ListBuckets)
|
baRouter.POST("/buckets", baseApi.ListBuckets)
|
||||||
withRecordRouter.POST("", baseApi.CreateBackup)
|
withRecordRouter.POST("", baseApi.CreateBackup)
|
||||||
withRecordRouter.POST("/del", baseApi.DeleteBackup)
|
withRecordRouter.POST("/del", baseApi.DeleteBackup)
|
||||||
|
withRecordRouter.POST("/record/download", baseApi.DownloadRecord)
|
||||||
withRecordRouter.POST("/record/del", baseApi.DeleteBackupRecord)
|
withRecordRouter.POST("/record/del", baseApi.DeleteBackupRecord)
|
||||||
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
|
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ export namespace Backup {
|
||||||
credential: string;
|
credential: string;
|
||||||
vars: string;
|
vars: string;
|
||||||
}
|
}
|
||||||
|
export interface RecordDownload {
|
||||||
|
source: string;
|
||||||
|
fileDir: string;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
export interface RecordInfo {
|
export interface RecordInfo {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
|
@ -16,6 +16,10 @@ export const editBackup = (params: Backup.BackupOperate) => {
|
||||||
export const deleteBackup = (params: { ids: number[] }) => {
|
export const deleteBackup = (params: { ids: number[] }) => {
|
||||||
return http.post(`/backups/del`, params);
|
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[] }) => {
|
export const deleteBackupRecord = (params: { ids: number[] }) => {
|
||||||
return http.post(`/backups/record/del`, params);
|
return http.post(`/backups/record/del`, params);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,8 @@ export default {
|
||||||
expand: '展开',
|
expand: '展开',
|
||||||
log: '日志',
|
log: '日志',
|
||||||
back: '返回',
|
back: '返回',
|
||||||
|
recover: '恢复',
|
||||||
|
download: '下载',
|
||||||
saveAndEnable: '保存并启用',
|
saveAndEnable: '保存并启用',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
show-overflow-tooltip
|
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>
|
</ComplexTable>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,10 +36,10 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
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 i18n from '@/lang';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { deleteBackupRecord } from '@/api/modules/backup';
|
import { deleteBackupRecord, downloadBackupRecord } from '@/api/modules/backup';
|
||||||
import { Backup } from '@/api/interface/backup';
|
import { Backup } from '@/api/interface/backup';
|
||||||
|
|
||||||
const selects = ref<any>([]);
|
const selects = ref<any>([]);
|
||||||
|
@ -87,6 +87,32 @@ const onBackup = async () => {
|
||||||
search();
|
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) => {
|
const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
||||||
let ids: Array<number> = [];
|
let ids: Array<number> = [];
|
||||||
if (row) {
|
if (row) {
|
||||||
|
@ -103,11 +129,22 @@ const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
label: i18n.global.t('commons.button.delete'),
|
label: i18n.global.t('commons.button.delete'),
|
||||||
icon: 'Delete',
|
|
||||||
click: (row: Backup.RecordInfo) => {
|
click: (row: Backup.RecordInfo) => {
|
||||||
onBatchDelete(row);
|
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({
|
defineExpose({
|
||||||
|
|
Loading…
Reference in New Issue