mirror of https://github.com/1Panel-dev/1Panel
feat: 完成数据库备份与计划任务联调
parent
9f1e417c06
commit
0f136570fe
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"])
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue