From f0d82f90047c6ab5f19bb9539f0346db84cf28f8 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 27 Oct 2022 23:09:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20mysql=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=A4=87=E4=BB=BD=E4=B8=8E=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/backup.go | 18 +++ backend/app/api/v1/databse_mysql.go | 49 ++++++++ backend/app/dto/backup.go | 15 +++ backend/app/dto/database.go | 20 ++- backend/app/model/backup.go | 10 ++ backend/app/repo/backup.go | 51 +++++++- backend/app/service/backup.go | 50 ++++++++ backend/app/service/database_mysql.go | 109 +++++++++++++--- backend/app/service/database_test.go | 75 +++-------- .../constant/{backup_account.go => backup.go} | 3 + backend/constant/container.go | 3 - backend/init/migration/migrations/init.go | 2 +- backend/router/ro_backup.go | 1 + backend/router/ro_database.go | 3 + frontend/src/api/interface/backup.ts | 7 ++ frontend/src/api/interface/database.ts | 13 ++ frontend/src/api/modules/backup.ts | 3 + frontend/src/api/modules/database.ts | 11 ++ frontend/src/lang/modules/zh.ts | 2 + .../src/views/database/mysql/backup/index.vue | 116 ++++++++++++++++++ frontend/src/views/database/mysql/index.vue | 15 ++- 21 files changed, 494 insertions(+), 82 deletions(-) rename backend/constant/{backup_account.go => backup.go} (66%) create mode 100644 frontend/src/views/database/mysql/backup/index.vue diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go index 9c4929524..10173c2e3 100644 --- a/backend/app/api/v1/backup.go +++ b/backend/app/api/v1/backup.go @@ -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 { diff --git a/backend/app/api/v1/databse_mysql.go b/backend/app/api/v1/databse_mysql.go index d342698df..5804fd7e0 100644 --- a/backend/app/api/v1/databse_mysql.go +++ b/backend/app/api/v1/databse_mysql.go @@ -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 { diff --git a/backend/app/dto/backup.go b/backend/app/dto/backup.go index c9370ee8c..c1320434c 100644 --- a/backend/app/dto/backup.go +++ b/backend/app/dto/backup.go @@ -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"` diff --git a/backend/app/dto/database.go b/backend/app/dto/database.go index e6be8c3f6..0c29567c4 100644 --- a/backend/app/dto/database.go +++ b/backend/app/dto/database.go @@ -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"` } diff --git a/backend/app/model/backup.go b/backend/app/model/backup.go index 720578163..54fc49d9f 100644 --- a/backend/app/model/backup.go +++ b/backend/app/model/backup.go @@ -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"` +} diff --git a/backend/app/repo/backup.go b/backend/app/repo/backup.go index b60038f9c..2e92912da 100644 --- a/backend/app/repo/backup.go +++ b/backend/app/repo/backup.go @@ -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 +} diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go index e6f9c8835..47c4b6c08 100644 --- a/backend/app/service/backup.go +++ b/backend/app/service/backup.go @@ -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) } diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 5afafd68d..dd3d72ab9 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -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 } diff --git a/backend/app/service/database_test.go b/backend/app/service/database_test.go index 8656f92c1..fc9cabdbc 100644 --- a/backend/app/service/database_test.go +++ b/backend/app/service/database_test.go @@ -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) } diff --git a/backend/constant/backup_account.go b/backend/constant/backup.go similarity index 66% rename from backend/constant/backup_account.go rename to backend/constant/backup.go index d2a688620..1e0f4b1e7 100644 --- a/backend/constant/backup_account.go +++ b/backend/constant/backup.go @@ -8,4 +8,7 @@ const ( OSS = "OSS" Sftp = "SFTP" MinIo = "MINIO" + + DatabaseDir = "/opt/1Panel/data/backup/database" + WebsiteDir = "/opt/1Panel/data/backup/website" ) diff --git a/backend/constant/container.go b/backend/constant/container.go index d668b425b..353179268 100644 --- a/backend/constant/container.go +++ b/backend/constant/container.go @@ -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" ) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index da03ad47e..8be7a5e97 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -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{ diff --git a/backend/router/ro_backup.go b/backend/router/ro_backup.go index ce89d375b..a90d1b37a 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/del", baseApi.DeleteBackupRecord) withRecordRouter.PUT(":id", baseApi.UpdateBackup) } } diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go index 7dd9fa218..e127aaf74 100644 --- a/backend/router/ro_database.go +++ b/backend/router/ro_database.go @@ -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) diff --git a/frontend/src/api/interface/backup.ts b/frontend/src/api/interface/backup.ts index 80b268a7a..64f0b2840 100644 --- a/frontend/src/api/interface/backup.ts +++ b/frontend/src/api/interface/backup.ts @@ -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; diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index dbbdee428..aaa15f399 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -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; diff --git a/frontend/src/api/modules/backup.ts b/frontend/src/api/modules/backup.ts index ba590b51f..c1588393d 100644 --- a/frontend/src/api/modules/backup.ts +++ b/frontend/src/api/modules/backup.ts @@ -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); diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index aec26eaa5..3c0345495 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -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>(`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>(`/databases/backups/search`, params); +}; + export const addMysqlDB = (params: Database.MysqlDBCreate) => { return http.post(`/databases`, params); }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 0fdc7e8c6..c7a973c25 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -152,6 +152,8 @@ export default { logout: '退出登录', }, database: { + source: '来源', + backup: '备份数据库', permission: '权限', permissionLocal: '本地服务器', permissionForIP: '指定 IP', diff --git a/frontend/src/views/database/mysql/backup/index.vue b/frontend/src/views/database/mysql/backup/index.vue new file mode 100644 index 000000000..d247cb2a1 --- /dev/null +++ b/frontend/src/views/database/mysql/backup/index.vue @@ -0,0 +1,116 @@ + + + diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index 17305f20b..b66bcfd76 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -107,12 +107,14 @@ +