From 9f1e417c064c539ae91853d95dff16a836a78af5 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Fri, 28 Oct 2022 11:02:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=A4=87=E4=BB=BD=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/backup.go | 19 ++++++++ backend/app/dto/backup.go | 6 +++ backend/app/service/backup.go | 42 +++++++++++++++++ backend/router/ro_backup.go | 1 + frontend/src/api/interface/backup.ts | 5 +++ frontend/src/api/modules/backup.ts | 4 ++ frontend/src/lang/modules/zh.ts | 2 + .../src/views/database/mysql/backup/index.vue | 45 +++++++++++++++++-- 8 files changed, 120 insertions(+), 4 deletions(-) diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go index 10173c2e3..25d66b142 100644 --- a/backend/app/api/v1/backup.go +++ b/backend/app/api/v1/backup.go @@ -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 { diff --git a/backend/app/dto/backup.go b/backend/app/dto/backup.go index c1320434c..6b377aa6d 100644 --- a/backend/app/dto/backup.go +++ b/backend/app/dto/backup.go @@ -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"` diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go index 47c4b6c08..a93598be8 100644 --- a/backend/app/service/backup.go +++ b/backend/app/service/backup.go @@ -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 { diff --git a/backend/router/ro_backup.go b/backend/router/ro_backup.go index a90d1b37a..08ec01aba 100644 --- a/backend/router/ro_backup.go +++ b/backend/router/ro_backup.go @@ -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) } diff --git a/frontend/src/api/interface/backup.ts b/frontend/src/api/interface/backup.ts index 64f0b2840..af21577e9 100644 --- a/frontend/src/api/interface/backup.ts +++ b/frontend/src/api/interface/backup.ts @@ -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; diff --git a/frontend/src/api/modules/backup.ts b/frontend/src/api/modules/backup.ts index c1588393d..cca67c22d 100644 --- a/frontend/src/api/modules/backup.ts +++ b/frontend/src/api/modules/backup.ts @@ -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(`/backups/record/download`, params, { responseType: 'blob' }); +}; export const deleteBackupRecord = (params: { ids: number[] }) => { return http.post(`/backups/record/del`, params); }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index c7a973c25..093719a7f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -27,6 +27,8 @@ export default { expand: '展开', log: '日志', back: '返回', + recover: '恢复', + download: '下载', saveAndEnable: '保存并启用', }, search: { diff --git a/frontend/src/views/database/mysql/backup/index.vue b/frontend/src/views/database/mysql/backup/index.vue index d247cb2a1..796b8b476 100644 --- a/frontend/src/views/database/mysql/backup/index.vue +++ b/frontend/src/views/database/mysql/backup/index.vue @@ -25,7 +25,7 @@ show-overflow-tooltip /> - + @@ -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([]); @@ -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 = []; 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({