feat: 完成 mysql 数据库备份与恢复功能

This commit is contained in:
ssongliu 2022-10-27 23:09:39 +08:00 committed by ssongliu
parent 325bb7bb5f
commit 8cf9c27f5f
21 changed files with 494 additions and 82 deletions

View File

@ -61,6 +61,24 @@ func (b *BaseApi) DeleteBackup(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) DeleteBackupRecord(c *gin.Context) {
var req dto.BatchDeleteReq
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
}
if err := backupService.BatchDeleteRecord(req.Ids); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateBackup(c *gin.Context) {
var req dto.BackupOperate
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -80,6 +80,55 @@ func (b *BaseApi) SearchMysql(c *gin.Context) {
})
}
func (b *BaseApi) SearchDBBackups(c *gin.Context) {
var req dto.SearchBackupsWithPage
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := mysqlService.SearchBacpupsWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) BackupMysql(c *gin.Context) {
var req dto.BackupDB
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := mysqlService.Backup(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) RecoverMysql(c *gin.Context) {
var req dto.RecoverDB
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := mysqlService.Recover(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) DeleteMysql(c *gin.Context) {
var req dto.BatchDeleteReq
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -17,6 +17,21 @@ type BackupInfo struct {
Vars string `json:"vars"`
}
type BackupSearch struct {
PageInfo
Type string `json:"type" validate:"required,oneof=website mysql"`
Name string `json:"name" validate:"required"`
DetailName string `json:"detailName"`
}
type BackupRecords struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Source string `json:"source"`
FileDir string `json:"fileDir"`
FileName string `json:"fileName"`
}
type ForBuckets struct {
Type string `json:"type" validate:"required"`
Credential string `json:"credential" validate:"required"`

View File

@ -10,6 +10,7 @@ type MysqlDBInfo struct {
Username string `json:"username"`
Password string `json:"password"`
Permission string `json:"permission"`
BackupCount int `json:"backupCount"`
Description string `json:"description"`
}
@ -115,5 +116,22 @@ type DBBaseInfo struct {
type SearchDBWithPage struct {
PageInfo
Version string `json:"version" validate:"required"`
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
}
type SearchBackupsWithPage struct {
PageInfo
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
DBName string `json:"dbName" validate:"required"`
}
type BackupDB struct {
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
DBName string `json:"dbName" validate:"required"`
}
type RecoverDB struct {
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
DBName string `json:"dbName" validate:"required"`
BackupName string `json:"backupName" validate:"required"`
}

View File

@ -7,3 +7,13 @@ type BackupAccount struct {
Credential string `gorm:"type:varchar(256)" json:"credential"`
Vars string `gorm:"type:longText" json:"vars"`
}
type BackupRecord struct {
BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"`
Name string `gorm:"type:varchar(64);not null" json:"name"`
DetailName string `gorm:"type:varchar(256)" json:"detailName"`
Source string `gorm:"type:varchar(256)" json:"source"`
FileDir string `gorm:"type:varchar(256)" json:"fileDir"`
FileName string `gorm:"type:varchar(256)" json:"fileName"`
}

View File

@ -3,16 +3,22 @@ package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
type BackupRepo struct{}
type IBackupRepo interface {
Get(opts ...DBOption) (model.BackupAccount, error)
ListRecord(opts ...DBOption) ([]model.BackupRecord, error)
PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error)
List(opts ...DBOption) ([]model.BackupAccount, error)
Create(backup *model.BackupAccount) error
CreateRecord(record *model.BackupRecord) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
DeleteRecord(opts ...DBOption) error
WithByDetailName(detailName string) DBOption
}
func NewIBackupRepo() IBackupRepo {
@ -29,14 +35,43 @@ func (u *BackupRepo) Get(opts ...DBOption) (model.BackupAccount, error) {
return backup, err
}
func (u *BackupRepo) ListRecord(opts ...DBOption) ([]model.BackupRecord, error) {
var users []model.BackupRecord
db := global.DB.Model(&model.BackupRecord{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&users).Error
return users, err
}
func (u *BackupRepo) PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) {
var users []model.BackupRecord
db := global.DB.Model(&model.BackupRecord{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (c *BackupRepo) WithByDetailName(detailName string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(detailName) == 0 {
return g
}
return g.Where("detail_name = ?", detailName)
}
}
func (u *BackupRepo) List(opts ...DBOption) ([]model.BackupAccount, error) {
var ops []model.BackupAccount
db := global.DB.Model(&model.BackupAccount{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Find(&ops).Error
return ops, err
}
@ -45,6 +80,10 @@ func (u *BackupRepo) Create(backup *model.BackupAccount) error {
return global.DB.Create(backup).Error
}
func (u *BackupRepo) CreateRecord(record *model.BackupRecord) error {
return global.DB.Create(record).Error
}
func (u *BackupRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.BackupAccount{}).Where("id = ?", id).Updates(vars).Error
}
@ -56,3 +95,11 @@ func (u *BackupRepo) Delete(opts ...DBOption) error {
}
return db.Delete(&model.BackupAccount{}).Error
}
func (u *BackupRepo) DeleteRecord(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.BackupRecord{}).Error
}

View File

@ -2,10 +2,12 @@ package service
import (
"encoding/json"
"os"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cloud_storage"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@ -15,10 +17,12 @@ type BackupService struct{}
type IBackupService interface {
List() ([]dto.BackupInfo, error)
SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error)
Create(backupDto dto.BackupOperate) error
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
Update(id uint, upMap map[string]interface{}) error
BatchDelete(ids []uint) error
BatchDeleteRecord(ids []uint) error
NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error)
}
@ -39,6 +43,25 @@ func (u *BackupService) List() ([]dto.BackupInfo, error) {
return dtobas, err
}
func (u *BackupService) SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error) {
total, records, err := backupRepo.PageRecord(
search.Page, search.PageSize,
commonRepo.WithOrderBy("created_at desc"),
commonRepo.WithByName(search.Name),
commonRepo.WithByType(search.Type),
backupRepo.WithByDetailName(search.DetailName),
)
var dtobas []dto.BackupRecords
for _, group := range records {
var item dto.BackupRecords
if err := copier.Copy(&item, &group); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtobas = append(dtobas, item)
}
return total, dtobas, err
}
func (u *BackupService) Create(backupDto dto.BackupOperate) error {
backup, _ := backupRepo.Get(commonRepo.WithByType(backupDto.Type))
if backup.ID != 0 {
@ -80,6 +103,33 @@ func (u *BackupService) BatchDelete(ids []uint) error {
return backupRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *BackupService) BatchDeleteRecord(ids []uint) error {
records, err := backupRepo.ListRecord(commonRepo.WithIdsIn(ids))
if err != nil {
return err
}
for _, record := range records {
if record.Source == "LOCAL" {
if err := os.Remove(record.FileDir + record.FileName); err != nil {
global.LOG.Errorf("remove file %s failed, err: %v", record.FileDir+record.FileName, err)
}
} else {
backupAccount, err := backupRepo.Get(commonRepo.WithByName(record.Source))
if err != nil {
return err
}
client, err := u.NewClient(&backupAccount)
if err != nil {
return err
}
if _, err = client.Delete(record.FileDir + record.FileName); err != nil {
global.LOG.Errorf("remove file %s from %s failed, err: %v", record.FileDir+record.FileName, record.Source, err)
}
}
}
return backupRepo.DeleteRecord(commonRepo.WithIdsIn(ids))
}
func (u *BackupService) Update(id uint, upMap map[string]interface{}) error {
return backupRepo.Update(id, upMap)
}

View File

@ -1,8 +1,10 @@
package service
import (
"compress/gzip"
"encoding/json"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
@ -11,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@ -20,9 +23,14 @@ type MysqlService struct{}
type IMysqlService interface {
SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error)
SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error)
Create(mysqlDto dto.MysqlDBCreate) error
ChangeInfo(info dto.ChangeDBInfo) error
UpdateVariables(variables dto.MysqlVariablesUpdate) error
Backup(db dto.BackupDB) error
Recover(db dto.RecoverDB) error
Delete(version string, ids []uint) error
LoadStatus(version string) (*dto.MysqlStatus, error)
LoadVariables(version string) (*dto.MysqlVariables, error)
@ -47,6 +55,21 @@ func (u *MysqlService) SearchWithPage(search dto.SearchDBWithPage) (int64, inter
return total, dtoMysqls, err
}
func (u *MysqlService) SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) {
app, err := mysqlRepo.LoadBaseInfoByVersion(search.Version)
if err != nil {
return 0, nil, err
}
searchDto := dto.BackupSearch{
Type: "database-mysql",
PageInfo: search.PageInfo,
Name: app.Name,
DetailName: search.DBName,
}
return NewIBackupService().SearchRecordWithPage(searchDto)
}
func (u *MysqlService) LoadRunningVersion() ([]string, error) {
return mysqlRepo.LoadRunningVersion()
}
@ -87,6 +110,66 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
return nil
}
func (u *MysqlService) Backup(db dto.BackupDB) error {
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version)
if err != nil {
return err
}
backupDir := fmt.Sprintf("%s/%s/%s/", constant.DatabaseDir, app.Name, db.DBName)
if _, err := os.Stat(backupDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(backupDir, os.ModePerm); err != nil {
return err
}
}
backupName := fmt.Sprintf("%s%s_%s.sql.gz", backupDir, db.DBName, time.Now().Format("20060102150405"))
outfile, _ := os.OpenFile(backupName, os.O_RDWR|os.O_CREATE, 0755)
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, db.DBName)
gzipCmd := exec.Command("gzip", "-cf")
gzipCmd.Stdin, _ = cmd.StdoutPipe()
gzipCmd.Stdout = outfile
_ = gzipCmd.Start()
_ = cmd.Run()
_ = gzipCmd.Wait()
if err := backupRepo.CreateRecord(&model.BackupRecord{
Type: "database-mysql",
Name: app.Name,
DetailName: db.DBName,
Source: "LOCAL",
FileDir: backupDir,
FileName: strings.ReplaceAll(backupName, backupDir, ""),
}); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)
}
return nil
}
func (u *MysqlService) Recover(db dto.RecoverDB) error {
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version)
if err != nil {
return err
}
gzipFile, err := os.Open(db.BackupName)
if err != nil {
fmt.Println(err)
}
defer gzipFile.Close()
gzipReader, err := gzip.NewReader(gzipFile)
if err != nil {
fmt.Println(err)
}
defer gzipReader.Close()
cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, db.DBName)
cmd.Stdin = gzipReader
stdout, err := cmd.CombinedOutput()
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
return errors.New(stdStr)
}
return nil
}
func (u *MysqlService) Delete(version string, ids []uint) error {
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
if err != nil {
@ -330,13 +413,13 @@ func (u *MysqlService) LoadStatus(version string) (*dto.MysqlStatus, error) {
}
func excuteSqlForMaps(containerName, password, command string) (map[string]string, error) {
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
if err != nil {
return nil, err
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
return nil, errors.New(stdStr)
}
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
rows := strings.Split(stdStr, "\n")
rowMap := make(map[string]string)
for _, v := range rows {
@ -349,25 +432,21 @@ func excuteSqlForMaps(containerName, password, command string) (map[string]strin
}
func excuteSqlForRows(containerName, password, command string) ([]string, error) {
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
return nil, errors.New(stdStr)
}
return strings.Split(stdStr, "\n"), nil
}
func excuteSql(containerName, password, command string) error {
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
if err != nil {
return err
}
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if strings.HasPrefix(string(stdStr), "ERROR ") {
return errors.New(string(stdStr))
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
return errors.New(stdStr)
}
return nil
}

View File

@ -1,71 +1,30 @@
package service
import (
"encoding/json"
"compress/gzip"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/backend/app/dto"
_ "github.com/go-sql-driver/mysql"
)
func TestMysql(t *testing.T) {
cmd := exec.Command("docker", "exec", "-i", "1Panel-mysql5.7-RnzE", "mysql", "-uroot", "-pCalong@2016", "-e", "show global variables;")
stdout, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err)
}
kk := strings.Split(string(stdout), "\n")
testMap := make(map[string]interface{})
for _, v := range kk {
itemRow := strings.Split(v, "\t")
if len(itemRow) == 2 {
testMap[itemRow[0]] = itemRow[1]
}
}
var info dto.MysqlVariables
arr, err := json.Marshal(testMap)
if err != nil {
fmt.Println(err)
}
_ = json.Unmarshal(arr, &info)
fmt.Print(info)
// fmt.Println(string(stdout))
// for {
// str, err := hr.Reader.ReadString('\n')
// if err == nil {
// testMap := make(map[string]interface{})
// err = json.Unmarshal([]byte(str), &testMap)
// fmt.Println(err)
// for k, v := range testMap {
// fmt.Println(k, v)
// }
// // fmt.Print(str)
// } else if err == io.EOF {
// // ReadString最后会同EOF和最后的数据一起返回
// fmt.Println(str)
// break
// } else {
// fmt.Println("出错!!")
// return
// }
// }
// input, err := hr.Reader.ReadString('\n')
// if err == nil {
// fmt.Printf("The input was: %s\n", input)
// }
// _, err = hr.Conn.Write([]byte("show global variables; \n"))
// if err != nil {
// fmt.Println(err)
// }
// time.Sleep(3 * time.Second)
// buf1 := make([]byte, 1024)
// _, err = hr.Reader.Read(buf1)
// if err != nil {
// fmt.Println(err)
// }
// fmt.Println(string(buf1))
gzipFile, err := os.Open("/tmp/ko.sql.gz")
if err != nil {
fmt.Println(err)
}
defer gzipFile.Close()
gzipReader, err := gzip.NewReader(gzipFile)
if err != nil {
fmt.Println(err)
}
defer gzipReader.Close()
cmd := exec.Command("docker", "exec", "-i", "365", "mysql", "-uroot", "-pCalong@2012", "kubeoperator")
cmd.Stdin = gzipReader
stdout, err := cmd.CombinedOutput()
fmt.Println(string(stdout), err)
}

View File

@ -8,4 +8,7 @@ const (
OSS = "OSS"
Sftp = "SFTP"
MinIo = "MINIO"
DatabaseDir = "/opt/1Panel/data/backup/database"
WebsiteDir = "/opt/1Panel/data/backup/website"
)

View File

@ -17,7 +17,4 @@ const (
DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json"
TmpDockerBuildDir = "/opt/1Panel/data/docker/build"
TmpComposeBuildDir = "/opt/1Panel/data/docker/compose"
ExecCmd = "docker exec"
ExecCmdIT = "docker exec -it"
)

View File

@ -126,7 +126,7 @@ var AddTableSetting = &gormigrate.Migration{
var AddTableBackupAccount = &gormigrate.Migration{
ID: "20200916-add-table-backup",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.BackupAccount{}); err != nil {
if err := tx.AutoMigrate(&model.BackupAccount{}, &model.BackupRecord{}); err != nil {
return err
}
item := &model.BackupAccount{

View File

@ -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/del", baseApi.DeleteBackupRecord)
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
}
}

View File

@ -23,6 +23,9 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
{
withRecordRouter.POST("", baseApi.CreateMysql)
withRecordRouter.PUT("/:id", baseApi.UpdateMysql)
withRecordRouter.POST("/backup", baseApi.BackupMysql)
withRecordRouter.POST("/recover", baseApi.RecoverMysql)
withRecordRouter.POST("/backups/search", baseApi.SearchDBBackups)
withRecordRouter.POST("/del", baseApi.DeleteMysql)
withRecordRouter.POST("/variables/update", baseApi.UpdateMysqlVariables)
cmdRouter.POST("/search", baseApi.SearchMysql)

View File

@ -13,6 +13,13 @@ export namespace Backup {
credential: string;
vars: string;
}
export interface RecordInfo {
id: number;
createdAt: Date;
source: string;
fileDir: string;
fileName: string;
}
export interface ForBucket {
type: string;
credential: string;

View File

@ -4,6 +4,19 @@ export namespace Database {
export interface Search extends ReqPage {
version: string;
}
export interface SearchBackupRecord extends ReqPage {
version: string;
dbName: string;
}
export interface Backup {
version: string;
dbName: string;
}
export interface Recover {
version: string;
dbName: string;
backupName: string;
}
export interface MysqlDBInfo {
id: number;
createdAt: Date;

View File

@ -16,6 +16,9 @@ export const editBackup = (params: Backup.BackupOperate) => {
export const deleteBackup = (params: { ids: number[] }) => {
return http.post(`/backups/del`, params);
};
export const deleteBackupRecord = (params: { ids: number[] }) => {
return http.post(`/backups/record/del`, params);
};
export const listBucket = (params: Backup.ForBucket) => {
return http.post(`/backups/buckets`, params);

View File

@ -1,11 +1,22 @@
import http from '@/api';
import { ResPage } from '../interface';
import { Backup } from '../interface/backup';
import { Database } from '../interface/database';
export const searchMysqlDBs = (params: Database.Search) => {
return http.post<ResPage<Database.MysqlDBInfo>>(`databases/search`, params);
};
export const backup = (params: Database.Backup) => {
return http.post(`/databases/backup`, params);
};
export const recover = (params: Database.Recover) => {
return http.post(`/databases/recover`, params);
};
export const searchBackupRecords = (params: Database.SearchBackupRecord) => {
return http.post<ResPage<Backup.RecordInfo>>(`/databases/backups/search`, params);
};
export const addMysqlDB = (params: Database.MysqlDBCreate) => {
return http.post(`/databases`, params);
};

View File

@ -152,6 +152,8 @@ export default {
logout: '退出登录',
},
database: {
source: '来源',
backup: '备份数据库',
permission: '权限',
permissionLocal: '本地服务器',
permissionForIP: '指定 IP',

View File

@ -0,0 +1,116 @@
<template>
<div>
<el-dialog v-model="backupVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>{{ $t('database.backup') }} - {{ dbName }}</span>
</div>
</template>
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
<template #toolbar>
<el-button type="primary" @click="onBackup()">
{{ $t('database.backup') }}
</el-button>
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" prop="fileName" show-overflow-tooltip />
<el-table-column :label="$t('database.source')" prop="source" />
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
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 i18n from '@/lang';
import { ElMessage } from 'element-plus';
import { deleteBackupRecord } from '@/api/modules/backup';
import { Backup } from '@/api/interface/backup';
const selects = ref<any>([]);
const data = ref();
const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
const backupVisiable = ref(false);
const version = ref();
const dbName = ref();
interface DialogProps {
version: string;
dbName: string;
}
const acceptParams = (params: DialogProps): void => {
version.value = params.version;
dbName.value = params.dbName;
backupVisiable.value = true;
search();
};
const search = async () => {
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
version: version.value,
dbName: dbName.value,
};
const res = await searchBackupRecords(params);
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
};
const onBackup = async () => {
let params = {
version: version.value,
dbName: dbName.value,
};
await backup(params);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
search();
};
const onBatchDelete = async (row: Backup.RecordInfo | null) => {
let ids: Array<number> = [];
if (row) {
ids.push(row.id);
} else {
selects.value.forEach((item: Backup.RecordInfo) => {
ids.push(item.id);
});
}
await useDeleteData(deleteBackupRecord, { ids: ids }, 'commons.msg.delete', true);
search();
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
icon: 'Delete',
click: (row: Backup.RecordInfo) => {
onBatchDelete(row);
},
},
];
defineExpose({
acceptParams,
});
</script>

View File

@ -107,12 +107,14 @@
</el-dialog>
<OperatrDialog @search="search" ref="dialogRef" />
<BackupRecords ref="dialogBackupRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import OperatrDialog from '@/views/database/mysql/create/index.vue';
import BackupRecords from '@/views/database/mysql/backup/index.vue';
import Setting from '@/views/database/mysql/setting/index.vue';
import Submenu from '@/views/database/index.vue';
import { dateFromat } from '@/utils/util';
@ -144,6 +146,15 @@ const onOpenDialog = async () => {
dialogRef.value!.acceptParams(params);
};
const dialogBackupRef = ref();
const onOpenBackupDialog = async (dbName: string) => {
let params = {
version: version.value,
dbName: dbName,
};
dialogBackupRef.value!.acceptParams(params);
};
const settingRef = ref();
const onSetting = async () => {
isOnSetting.value = true;
@ -252,9 +263,9 @@ const buttons = [
},
},
{
label: i18n.global.t('database.backupList') + '(1)',
label: i18n.global.t('database.backupList'),
click: (row: Database.MysqlDBInfo) => {
onBatchDelete(row);
onOpenBackupDialog(row.name);
},
},
{