feat: 完成数据库备份与计划任务联调

pull/34/head
ssongliu 2022-10-28 18:46:14 +08:00 committed by ssongliu
parent 9f1e417c06
commit 0f136570fe
16 changed files with 275 additions and 136 deletions

View File

@ -103,21 +103,6 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) {
return return
} }
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["script"] = req.Script
upMap["specType"] = req.SpecType
upMap["week"] = req.Week
upMap["day"] = req.Day
upMap["hour"] = req.Hour
upMap["minute"] = req.Minute
upMap["website"] = req.Website
upMap["exclusionRules"] = req.ExclusionRules
upMap["database"] = req.Database
upMap["url"] = req.URL
upMap["sourceDir"] = req.SourceDir
upMap["targetDirID"] = req.TargetDirID
upMap["retainCopies"] = req.RetainCopies
if err := cronjobService.Update(id, req); err != nil { if err := cronjobService.Update(id, req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return

View File

@ -80,6 +80,22 @@ func (b *BaseApi) SearchMysql(c *gin.Context) {
}) })
} }
func (b *BaseApi) ListDBNameByVersion(c *gin.Context) {
version, ok := c.Params.Get("version")
if !ok {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error version in path"))
return
}
list, err := mysqlService.ListDBByVersion(version)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) SearchDBBackups(c *gin.Context) { func (b *BaseApi) SearchDBBackups(c *gin.Context) {
var req dto.SearchBackupsWithPage var req dto.SearchBackupsWithPage
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@ -87,7 +103,7 @@ func (b *BaseApi) SearchDBBackups(c *gin.Context) {
return return
} }
total, list, err := mysqlService.SearchBacpupsWithPage(req) total, list, err := mysqlService.SearchBackupsWithPage(req)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return

View File

@ -15,8 +15,10 @@ type CronjobCreate struct {
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
Database string `json:"database"` Database string `json:"database"`
DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`
KeepLocal bool `json:"keepLocal"`
TargetDirID int `json:"targetDirID"` TargetDirID int `json:"targetDirID"`
RetainCopies int `json:"retainCopies" validate:"number,min=1"` RetainCopies int `json:"retainCopies" validate:"number,min=1"`
} }
@ -33,8 +35,10 @@ type CronjobUpdate struct {
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
Database string `json:"database"` Database string `json:"database"`
DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`
KeepLocal bool `json:"keepLocal"`
TargetDirID int `json:"targetDirID"` TargetDirID int `json:"targetDirID"`
RetainCopies int `json:"retainCopies" validate:"number,min=1"` RetainCopies int `json:"retainCopies" validate:"number,min=1"`
} }
@ -63,8 +67,10 @@ type CronjobInfo struct {
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
Database string `json:"database"` Database string `json:"database"`
DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`
KeepLocal bool `json:"keepLocal"`
TargetDir string `json:"targetDir"` TargetDir string `json:"targetDir"`
TargetDirID int `json:"targetDirID"` TargetDirID int `json:"targetDirID"`
RetainCopies int `json:"retainCopies"` RetainCopies int `json:"retainCopies"`

View File

@ -17,11 +17,14 @@ type Cronjob struct {
Script string `gorm:"longtext" json:"script"` Script string `gorm:"longtext" json:"script"`
Website string `gorm:"type:varchar(64)" json:"website"` Website string `gorm:"type:varchar(64)" json:"website"`
Database string `gorm:"type:varchar(64)" json:"database"` Database string `gorm:"type:varchar(64)" json:"database"`
DBName string `gorm:"type:varchar(64)" json:"dbName"`
URL string `gorm:"type:varchar(256)" json:"url"` URL string `gorm:"type:varchar(256)" json:"url"`
SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"` SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"`
TargetDirID uint64 `gorm:"type:decimal" json:"targetDirID"`
ExclusionRules string `gorm:"longtext" json:"exclusionRules"` ExclusionRules string `gorm:"longtext" json:"exclusionRules"`
RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"`
KeepLocal bool `gorm:"type:varchar(64)" json:"keepLocal"`
TargetDirID uint64 `gorm:"type:decimal" json:"targetDirID"`
RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"`
Status string `gorm:"type:varchar(64)" json:"status"` Status string `gorm:"type:varchar(64)" json:"status"`
EntryID uint64 `gorm:"type:decimal" json:"entryID"` EntryID uint64 `gorm:"type:decimal" json:"entryID"`
@ -35,6 +38,8 @@ type JobRecords struct {
StartTime time.Time `gorm:"type:datetime" json:"startTime"` StartTime time.Time `gorm:"type:datetime" json:"startTime"`
Interval float64 `gorm:"type:float" json:"interval"` Interval float64 `gorm:"type:float" json:"interval"`
Records string `gorm:"longtext" json:"records"` Records string `gorm:"longtext" json:"records"`
FromLocal bool `gorm:"type:varchar(64)" json:"source"`
File string `gorm:"type:varchar(256)" json:"file"`
Status string `gorm:"type:varchar(64)" json:"status"` Status string `gorm:"type:varchar(64)" json:"status"`
Message string `gorm:"longtext" json:"message"` Message string `gorm:"longtext" json:"message"`
} }

View File

@ -66,7 +66,7 @@ func (u *BackupService) SearchRecordWithPage(search dto.BackupSearch) (int64, []
func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) { func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) {
if info.Source == "LOCAL" { if info.Source == "LOCAL" {
return info.FileDir + info.FileName, nil return info.FileDir + "/" + info.FileName, nil
} }
backup, _ := backupRepo.Get(commonRepo.WithByType(info.Source)) backup, _ := backupRepo.Get(commonRepo.WithByType(info.Source))
if backup.ID == 0 { if backup.ID == 0 {
@ -200,3 +200,25 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl
return backClient, nil return backClient, nil
} }
func loadLocalDir(backup model.BackupAccount) (string, error) {
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
return "", err
}
if _, ok := varMap["dir"]; !ok {
return "", errors.New("load local backup dir failed")
}
baseDir, ok := varMap["dir"].(string)
if ok {
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(baseDir, os.ModePerm); err != nil {
if err != nil {
return "", fmt.Errorf("mkdir %s failed, err: %v", baseDir, err)
}
}
}
return baseDir, nil
}
return "", fmt.Errorf("error type dir: %T", varMap["dir"])
}

View File

@ -217,10 +217,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
upMap["website"] = req.Website upMap["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules upMap["exclusion_rules"] = req.ExclusionRules
upMap["database"] = req.Database upMap["database"] = req.Database
upMap["db_name"] = req.DBName
upMap["url"] = req.URL upMap["url"] = req.URL
upMap["source_dir"] = req.SourceDir upMap["source_dir"] = req.SourceDir
upMap["keep_local"] = req.KeepLocal
upMap["target_dir_id"] = req.TargetDirID upMap["target_dir_id"] = req.TargetDirID
upMap["retain_days"] = req.RetainCopies upMap["retain_copies"] = req.RetainCopies
return cronjobRepo.Update(id, upMap) return cronjobRepo.Update(id, upMap)
} }
@ -252,7 +254,7 @@ func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) {
} }
func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) { func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) {
dir := fmt.Sprintf("%s%s/%s-%v", constant.TaskDir, cronjob.Type, cronjob.Name, cronjob.ID) dir := fmt.Sprintf("%s/%s/%s-%v", constant.TaskDir, cronjob.Type, cronjob.Name, cronjob.ID)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil { if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return "", err return "", err

View File

@ -2,7 +2,6 @@ package service
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -25,19 +24,23 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
err error err error
) )
record := cronjobRepo.StartRecords(cronjob.ID, "") record := cronjobRepo.StartRecords(cronjob.ID, "")
record.FromLocal = cronjob.KeepLocal
switch cronjob.Type { switch cronjob.Type {
case "shell": case "shell":
cmd := exec.Command(cronjob.Script) cmd := exec.Command(cronjob.Script)
message, err = cmd.CombinedOutput() stdout, errExec := cmd.CombinedOutput()
if errExec != nil {
err = errors.New(string(stdout))
}
case "website": case "website":
message, err = u.HandleBackup(cronjob, record.StartTime) record.File, err = u.HandleBackup(cronjob, record.StartTime)
case "database": case "database":
message, err = u.HandleBackup(cronjob, record.StartTime) record.File, err = u.HandleBackup(cronjob, record.StartTime)
case "directory": case "directory":
if len(cronjob.SourceDir) == 0 { if len(cronjob.SourceDir) == 0 {
return return
} }
message, err = u.HandleBackup(cronjob, record.StartTime) record.File, err = u.HandleBackup(cronjob, record.StartTime)
case "curl": case "curl":
if len(cronjob.URL) == 0 { if len(cronjob.URL) == 0 {
return return
@ -65,56 +68,63 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records) cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
} }
func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Time) ([]byte, error) { func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Time) (string, error) {
var stdout []byte var (
baseDir string
backupDir string
fileName string
)
backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID))) backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID)))
if err != nil { if err != nil {
return nil, err return "", err
} }
commonDir := fmt.Sprintf("%s/%s/", cronjob.Type, cronjob.Name) if cronjob.KeepLocal || cronjob.Type != "LOCAL" {
name := fmt.Sprintf("%s.gz", startTime.Format("20060102150405")) backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
if cronjob.Type != "database" {
name = fmt.Sprintf("%s.tar.gz", startTime.Format("20060102150405"))
}
if backup.Type == "LOCAL" {
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
return nil, err
}
if _, ok := varMap["dir"]; !ok {
return nil, errors.New("load local backup dir failed")
}
baseDir := varMap["dir"].(string)
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(baseDir, os.ModePerm); err != nil {
if err != nil {
return nil, fmt.Errorf("mkdir %s failed, err: %v", baseDir, err)
}
}
}
stdout, err = handleTar(cronjob.SourceDir, fmt.Sprintf("%s/%s", baseDir, commonDir), name, cronjob.ExclusionRules)
if err != nil { if err != nil {
return stdout, err return "", err
} }
u.HandleRmExpired(backup.Type, fmt.Sprintf("%s/%s", baseDir, commonDir), cronjob, nil) localDir, err := loadLocalDir(backupLocal)
return stdout, nil if err != nil {
return "", err
}
baseDir = localDir
} else {
baseDir = constant.TmpDir
}
if cronjob.Type == "database" {
fileName = fmt.Sprintf("db_%s_%s.sql.gz", cronjob.DBName, time.Now().Format("20060102150405"))
backupDir = fmt.Sprintf("database/%s/%s", cronjob.Database, cronjob.DBName)
err = backupMysql(backup.Type, baseDir, backupDir, cronjob.Database, cronjob.DBName, fileName)
if err != nil {
return "", err
}
} else {
fileName = fmt.Sprintf("%s.tar.gz", startTime.Format("20060102150405"))
backupDir = fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name)
if err := handleTar(cronjob.SourceDir, baseDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil {
return "", err
}
}
if backup.Type == "LOCAL" {
u.HandleRmExpired(backup.Type, baseDir, backupDir, cronjob, nil)
return baseDir + "/" + backupDir + "/" + fileName, nil
}
cloudFile := baseDir + "/" + backupDir + "/" + fileName
if !cronjob.KeepLocal {
cloudFile = backupDir + "/" + fileName
} }
targetDir := constant.TmpDir + commonDir
client, err := NewIBackupService().NewClient(&backup) client, err := NewIBackupService().NewClient(&backup)
if err != nil { if err != nil {
return nil, err return cloudFile, err
} }
if cronjob.Type != "database" { if _, err = client.Upload(baseDir+"/"+backupDir+"/"+fileName, backupDir+"/"+fileName); err != nil {
stdout, err = handleTar(cronjob.SourceDir, targetDir, name, cronjob.ExclusionRules) return cloudFile, err
if err != nil {
return stdout, err
}
} }
if _, err = client.Upload(targetDir+name, commonDir+name); err != nil { u.HandleRmExpired(backup.Type, baseDir, backupDir, cronjob, client)
return nil, err return cloudFile, nil
}
u.HandleRmExpired(backup.Type, commonDir+name, cronjob, client)
return stdout, nil
} }
func (u *CronjobService) HandleDelete(id uint) error { func (u *CronjobService) HandleDelete(id uint) error {
@ -132,26 +142,27 @@ func (u *CronjobService) HandleDelete(id uint) error {
return nil return nil
} }
func (u *CronjobService) HandleRmExpired(backType, path string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) { func (u *CronjobService) HandleRmExpired(backType, baseDir, backupDir string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) {
if backType != "LOCAL" { if backType != "LOCAL" {
commonDir := fmt.Sprintf("%s/%s/", cronjob.Type, cronjob.Name) currentObjs, err := backClient.ListObjects(backupDir + "/")
currentObjs, err := backClient.ListObjects(commonDir)
if err != nil { if err != nil {
global.LOG.Errorf("list bucket object %s failed, err: %v", commonDir, err) global.LOG.Errorf("list bucket object %s failed, err: %v", backupDir, err)
return return
} }
for i := 0; i < len(currentObjs)-int(cronjob.RetainCopies); i++ { for i := 0; i < len(currentObjs)-int(cronjob.RetainCopies); i++ {
_, _ = backClient.Delete(currentObjs[i].(string)) _, _ = backClient.Delete(currentObjs[i].(string))
} }
return if !cronjob.KeepLocal {
return
}
} }
files, err := ioutil.ReadDir(path) files, err := ioutil.ReadDir(baseDir + "/" + backupDir)
if err != nil { if err != nil {
global.LOG.Errorf("read dir %s failed, err: %v", path, err) global.LOG.Errorf("read dir %s failed, err: %v", baseDir+"/"+backupDir, err)
return return
} }
for i := 0; i < len(files)-int(cronjob.RetainCopies); i++ { for i := 0; i < len(files)-int(cronjob.RetainCopies); i++ {
_ = os.Remove(path + "/" + files[i].Name()) _ = os.Remove(baseDir + "/" + backupDir + "/" + files[i].Name())
} }
records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID))) records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)))
if len(records) > int(cronjob.RetainCopies) { if len(records) > int(cronjob.RetainCopies) {
@ -161,15 +172,15 @@ func (u *CronjobService) HandleRmExpired(backType, path string, cronjob *model.C
} }
} }
func handleTar(sourceDir, targetDir, name, exclusionRules string) ([]byte, error) { func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
return nil, err return err
} }
} }
exStr := []string{} exStr := []string{}
exStr = append(exStr, "zcvf") exStr = append(exStr, "zcvf")
exStr = append(exStr, targetDir+name) exStr = append(exStr, targetDir+"/"+name)
excludes := strings.Split(exclusionRules, ";") excludes := strings.Split(exclusionRules, ";")
for _, exclude := range excludes { for _, exclude := range excludes {
if len(exclude) == 0 { if len(exclude) == 0 {
@ -188,5 +199,9 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) ([]byte, error
exStr = append(exStr, sourceDir) exStr = append(exStr, sourceDir)
} }
cmd := exec.Command("tar", exStr...) cmd := exec.Command("tar", exStr...)
return (cmd.CombinedOutput()) stdout, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(stdout))
}
return nil
} }

View File

@ -23,7 +23,8 @@ type MysqlService struct{}
type IMysqlService interface { type IMysqlService interface {
SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error)
SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) ListDBByVersion(version string) ([]string, error)
SearchBackupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error)
Create(mysqlDto dto.MysqlDBCreate) error Create(mysqlDto dto.MysqlDBCreate) error
ChangeInfo(info dto.ChangeDBInfo) error ChangeInfo(info dto.ChangeDBInfo) error
UpdateVariables(variables dto.MysqlVariablesUpdate) error UpdateVariables(variables dto.MysqlVariablesUpdate) error
@ -55,7 +56,16 @@ func (u *MysqlService) SearchWithPage(search dto.SearchDBWithPage) (int64, inter
return total, dtoMysqls, err return total, dtoMysqls, err
} }
func (u *MysqlService) SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) { func (u *MysqlService) ListDBByVersion(version string) ([]string, error) {
mysqls, err := mysqlRepo.List(mysqlRepo.WithByVersion(version))
var dbNames []string
for _, mysql := range mysqls {
dbNames = append(dbNames, mysql.Name)
}
return dbNames, err
}
func (u *MysqlService) SearchBackupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) {
app, err := mysqlRepo.LoadBaseInfoByVersion(search.Version) app, err := mysqlRepo.LoadBaseInfoByVersion(search.Version)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
@ -111,36 +121,18 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
} }
func (u *MysqlService) Backup(db dto.BackupDB) error { func (u *MysqlService) Backup(db dto.BackupDB) error {
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version) backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
if err != nil { if err != nil {
return err return err
} }
localDir, err := loadLocalDir(backupLocal)
backupDir := fmt.Sprintf("%s/%s/%s/", constant.DatabaseDir, app.Name, db.DBName) if err != nil {
if _, err := os.Stat(backupDir); err != nil && os.IsNotExist(err) { return 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")) backupDir := fmt.Sprintf("database/%s/%s", db.Version, db.DBName)
outfile, _ := os.OpenFile(backupName, os.O_RDWR|os.O_CREATE, 0755) fileName := fmt.Sprintf("%s_%s.sql.gz", db.DBName, time.Now().Format("20060102150405"))
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, db.DBName) if err := backupMysql("LOCAL", localDir, backupDir, db.Version, db.DBName, fileName); err != nil {
gzipCmd := exec.Command("gzip", "-cf") return err
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 return nil
} }
@ -450,3 +442,44 @@ func excuteSql(containerName, password, command string) error {
} }
return nil return nil
} }
func backupMysql(backupType, baseDir, backupDir, version, dbName, fileName string) error {
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
if err != nil {
return err
}
fullDir := baseDir + "/" + backupDir
if _, err := os.Stat(fullDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(fullDir, os.ModePerm); err != nil {
if err != nil {
return fmt.Errorf("mkdir %s failed, err: %v", fullDir, err)
}
}
}
outfile, _ := os.OpenFile(fullDir+"/"+fileName, os.O_RDWR|os.O_CREATE, 0755)
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, dbName)
gzipCmd := exec.Command("gzip", "-cf")
gzipCmd.Stdin, _ = cmd.StdoutPipe()
gzipCmd.Stdout = outfile
_ = gzipCmd.Start()
_ = cmd.Run()
_ = gzipCmd.Wait()
record := &model.BackupRecord{
Type: "database-mysql",
Name: app.Name,
DetailName: dbName,
Source: backupType,
FileDir: backupDir,
FileName: fileName,
}
if baseDir != constant.TmpDir || backupType == "LOCAL" {
record.Source = "LOCAL"
record.FileDir = fullDir
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)
}
return nil
}

View File

@ -9,6 +9,6 @@ const (
Sftp = "SFTP" Sftp = "SFTP"
MinIo = "MINIO" MinIo = "MINIO"
DatabaseDir = "/opt/1Panel/data/backup/database" DatabaseBackupDir = "/opt/1Panel/data/backup/database"
WebsiteDir = "/opt/1Panel/data/backup/website" WebsiteBackupDir = "/opt/1Panel/data/backup/website"
) )

View File

@ -1,7 +1,7 @@
package constant package constant
const ( const (
TmpDir = "/opt/1Panel/task/tmp/" TmpDir = "/opt/1Panel/data/tmp"
TaskDir = "/opt/1Panel/task/" TaskDir = "/opt/1Panel/data/task"
DownloadDir = "/opt/1Panel/download/" DownloadDir = "/opt/1Panel/download"
) )

View File

@ -33,5 +33,6 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
cmdRouter.GET("/status/:version", baseApi.LoadStatus) cmdRouter.GET("/status/:version", baseApi.LoadStatus)
cmdRouter.GET("/baseinfo/:version", baseApi.LoadBaseinfo) cmdRouter.GET("/baseinfo/:version", baseApi.LoadBaseinfo)
cmdRouter.GET("/versions", baseApi.LoadVersions) cmdRouter.GET("/versions", baseApi.LoadVersions)
cmdRouter.GET("/dbs/:version", baseApi.ListDBNameByVersion)
} }
} }

View File

@ -15,8 +15,10 @@ export namespace Cronjob {
website: string; website: string;
exclusionRules: string; exclusionRules: string;
database: string; database: string;
dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;
keepLocal: boolean;
targetDirID: number; targetDirID: number;
targetDir: string; targetDir: string;
retainCopies: number; retainCopies: number;
@ -35,8 +37,10 @@ export namespace Cronjob {
website: string; website: string;
exclusionRules: string; exclusionRules: string;
database: string; database: string;
dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;
keepLocal: boolean;
targetDirID: number; targetDirID: number;
retainCopies: number; retainCopies: number;
} }
@ -52,8 +56,10 @@ export namespace Cronjob {
website: string; website: string;
exclusionRules: string; exclusionRules: string;
database: string; database: string;
dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;
keepLocal: boolean;
targetDirID: number; targetDirID: number;
retainCopies: number; retainCopies: number;
} }

View File

@ -6,6 +6,9 @@ import { Database } from '../interface/database';
export const searchMysqlDBs = (params: Database.Search) => { export const searchMysqlDBs = (params: Database.Search) => {
return http.post<ResPage<Database.MysqlDBInfo>>(`databases/search`, params); return http.post<ResPage<Database.MysqlDBInfo>>(`databases/search`, params);
}; };
export const listDBByVersion = (params: string) => {
return http.get(`databases/dbs/${params}`);
};
export const backup = (params: Database.Backup) => { export const backup = (params: Database.Backup) => {
return http.post(`/databases/backup`, params); return http.post(`/databases/backup`, params);

View File

@ -136,6 +136,7 @@ const onOpenDialog = async (
day: 1, day: 1,
hour: 2, hour: 2,
minute: 3, minute: 3,
keepLocal: true,
retainCopies: 7, retainCopies: 7,
}, },
) => { ) => {

View File

@ -74,13 +74,20 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item
v-if="dialogData.rowData!.type === 'database'" <div v-if="dialogData.rowData!.type === 'database'">
:label="$t('cronjob.database')" <el-form-item :label="$t('cronjob.database')" prop="database">
prop="database" <el-radio-group v-model="dialogData.rowData!.database" @change="changeDBVersion" class="ml-4">
> <el-radio v-for="item in mysqlVersionOptions" :key="item" :label="item" :value="item" />
<el-input style="width: 100%" clearable v-model="dialogData.rowData!.database" /> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item :label="$t('cronjob.database')" prop="dbName">
<el-select style="width: 100%" clearable v-model="dialogData.rowData!.dbName">
<el-option v-for="item in dbOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</div>
<el-form-item <el-form-item
v-if="dialogData.rowData!.type === 'directory'" v-if="dialogData.rowData!.type === 'directory'"
:label="$t('cronjob.sourceDir')" :label="$t('cronjob.sourceDir')"
@ -98,19 +105,30 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="isBackup()" :label="$t('cronjob.target')" prop="targetDirID"> <div v-if="isBackup()">
<el-select style="width: 100%" v-model="dialogData.rowData!.targetDirID"> <el-form-item :label="$t('cronjob.target')" prop="targetDirID">
<el-option <el-select style="width: 100%" v-model="dialogData.rowData!.targetDirID">
v-for="item in backupOptions" <el-option
:key="item.label" v-for="item in backupOptions"
:value="item.value" :key="item.label"
:label="item.label" :value="item.value"
/> :label="item.label"
</el-select> />
</el-form-item> </el-select>
<el-form-item v-if="isBackup()" :label="$t('cronjob.retainCopies')" prop="retainCopies"> </el-form-item>
<el-input-number :min="1" :max="30" v-model.number="dialogData.rowData!.retainCopies"></el-input-number> <el-form-item v-if="dialogData.rowData!.targetDirID !== localDirID">
</el-form-item> <el-checkbox v-model="dialogData.rowData!.keepLocal">
同时保留本地备份和云存储保留份数一致
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('cronjob.retainCopies')" prop="retainCopies">
<el-input-number
:min="1"
:max="30"
v-model.number="dialogData.rowData!.retainCopies"
></el-input-number>
</el-form-item>
</div>
<el-form-item v-if="dialogData.rowData!.type === 'curl'" :label="$t('cronjob.url') + 'URL'" prop="url"> <el-form-item v-if="dialogData.rowData!.type === 'curl'" :label="$t('cronjob.url') + 'URL'" prop="url">
<el-input style="width: 100%" clearable v-model="dialogData.rowData!.url" /> <el-input style="width: 100%" clearable v-model="dialogData.rowData!.url" />
@ -143,7 +161,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { loadBackupName } from '@/views/setting/helper'; import { loadBackupName } from '@/views/setting/helper';
import FileList from '@/components/file-list/index.vue'; import FileList from '@/components/file-list/index.vue';
@ -152,6 +170,7 @@ import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus'; import { ElForm, ElMessage } from 'element-plus';
import { Cronjob } from '@/api/interface/cronjob'; import { Cronjob } from '@/api/interface/cronjob';
import { addCronjob, editCronjob } from '@/api/modules/cronjob'; import { addCronjob, editCronjob } from '@/api/modules/cronjob';
import { listDBByVersion, loadVersions } from '@/api/modules/database';
interface DialogProps { interface DialogProps {
title: string; title: string;
@ -167,8 +186,14 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value = params; dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title); title.value = i18n.global.t('commons.button.' + dialogData.value.title);
cronjobVisiable.value = true; cronjobVisiable.value = true;
loadRunningOptions();
loadBackups();
}; };
const mysqlVersionOptions = ref();
const dbOptions = ref();
const localDirID = ref();
const websiteOptions = ref([ const websiteOptions = ref([
{ label: '所有', value: 'all' }, { label: '所有', value: 'all' },
{ label: '网站1', value: 'web1' }, { label: '网站1', value: 'web1' },
@ -263,6 +288,7 @@ const rules = reactive({
script: [Rules.requiredInput], script: [Rules.requiredInput],
website: [Rules.requiredSelect], website: [Rules.requiredSelect],
database: [Rules.requiredSelect], database: [Rules.requiredSelect],
dbName: [Rules.requiredSelect],
url: [Rules.requiredInput], url: [Rules.requiredInput],
sourceDir: [Rules.requiredSelect], sourceDir: [Rules.requiredSelect],
targetDirID: [Rules.requiredSelect, Rules.number], targetDirID: [Rules.requiredSelect, Rules.number],
@ -280,9 +306,30 @@ const loadBackups = async () => {
const res = await getBackupList(); const res = await getBackupList();
backupOptions.value = []; backupOptions.value = [];
for (const item of res.data) { for (const item of res.data) {
if (item.type === 'LOCAL') {
localDirID.value = item.id;
}
backupOptions.value.push({ label: loadBackupName(item.type), value: item.id }); backupOptions.value.push({ label: loadBackupName(item.type), value: item.id });
} }
}; };
const loadRunningOptions = async () => {
const res = await loadVersions();
mysqlVersionOptions.value = res.data;
if (mysqlVersionOptions.value.length != 0) {
dialogData.value.rowData!.database = mysqlVersionOptions.value[0];
changeDBVersion();
}
};
const changeDBVersion = async () => {
dialogData.value.rowData!.dbName = '';
const res = await listDBByVersion(dialogData.value.rowData!.database);
dbOptions.value = res.data;
if (dbOptions.value.length != 0) {
dialogData.value.rowData!.dbName = dbOptions.value[0];
}
};
function isBackup() { function isBackup() {
return ( return (
dialogData.value.rowData!.type === 'website' || dialogData.value.rowData!.type === 'website' ||
@ -328,9 +375,6 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
}); });
}; };
onMounted(() => {
loadBackups();
});
defineExpose({ defineExpose({
acceptParams, acceptParams,
}); });

View File

@ -126,7 +126,7 @@
<el-form-item :label="$t('cronjob.target')"> <el-form-item :label="$t('cronjob.target')">
{{ loadBackupName(dialogData.rowData!.targetDir) }} {{ loadBackupName(dialogData.rowData!.targetDir) }}
<el-button <el-button
v-if="currentRecord?.records! !== 'errHandle'" v-if="currentRecord?.status! !== 'Failed'"
type="primary" type="primary"
style="margin-left: 10px" style="margin-left: 10px"
link link