feat: 计划任务支持系统快照 (#2061)

Refs #1503 #1480 #1291
This commit is contained in:
ssongliu 2023-08-24 22:26:16 +08:00 committed by GitHub
parent 684f20a5dc
commit 6a8bd490bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 328 additions and 279 deletions

View file

@ -76,14 +76,12 @@ type PortUpdate struct {
} }
type SnapshotStatus struct { type SnapshotStatus struct {
Panel string `json:"panel"` Panel string `json:"panel"`
PanelCtl string `json:"panelCtl"` PanelInfo string `json:"panelInfo"`
PanelService string `json:"panelService"` DaemonJson string `json:"daemonJson"`
PanelInfo string `json:"panelInfo"` AppData string `json:"appData"`
DaemonJson string `json:"daemonJson"` PanelData string `json:"panelData"`
AppData string `json:"appData"` BackupData string `json:"backupData"`
PanelData string `json:"panelData"`
BackupData string `json:"backupData"`
Compress string `json:"compress"` Compress string `json:"compress"`
Upload string `json:"upload"` Upload string `json:"upload"`

View file

@ -20,15 +20,13 @@ type Snapshot struct {
type SnapshotStatus struct { type SnapshotStatus struct {
BaseModel BaseModel
SnapID uint `gorm:"type:decimal" json:"snapID"` SnapID uint `gorm:"type:decimal" json:"snapID"`
Panel string `json:"panel" gorm:"type:varchar(64);default:Running"` Panel string `json:"panel" gorm:"type:varchar(64);default:Running"`
PanelCtl string `json:"panelCtl" gorm:"type:varchar(64);default:Running"` PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"`
PanelService string `json:"panelService" gorm:"type:varchar(64);default:Running"` DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"`
PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"` AppData string `json:"appData" gorm:"type:varchar(64);default:Running"`
DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"` PanelData string `json:"panelData" gorm:"type:varchar(64);default:Running"`
AppData string `json:"appData" gorm:"type:varchar(64);default:Running"` BackupData string `json:"backupData" gorm:"type:varchar(64);default:Running"`
PanelData string `json:"panelData" gorm:"type:varchar(64);default:Running"`
BackupData string `json:"backupData" gorm:"type:varchar(64);default:Running"`
Compress string `json:"compress" gorm:"type:varchar(64);default:Waiting"` Compress string `json:"compress" gorm:"type:varchar(64);default:Waiting"`
Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"` Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"`

View file

@ -46,7 +46,7 @@ func (u *CronjobService) SearchWithPage(search dto.SearchWithPage) (int64, inter
if err := copier.Copy(&item, &cronjob); err != nil { if err := copier.Copy(&item, &cronjob); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
} }
if item.Type == "app" || item.Type == "website" || item.Type == "database" || item.Type == "directory" { if item.Type == "app" || item.Type == "website" || item.Type == "database" || item.Type == "directory" || item.Type == "snapshot" {
backup, _ := backupRepo.Get(commonRepo.WithByID(uint(item.TargetDirID))) backup, _ := backupRepo.Get(commonRepo.WithByID(uint(item.TargetDirID)))
if len(backup.Type) != 0 { if len(backup.Type) != 0 {
item.TargetDir = backup.Type item.TargetDir = backup.Type
@ -103,7 +103,7 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
if err != nil { if err != nil {
return err return err
} }
if req.CleanData && (cronjob.Type == "app" || cronjob.Type == "database" || cronjob.Type == "website" || cronjob.Type == "directory") { if req.CleanData && (cronjob.Type == "app" || cronjob.Type == "database" || cronjob.Type == "website" || cronjob.Type == "directory" || cronjob.Type == "snapshot") {
cronjob.RetainCopies = 0 cronjob.RetainCopies = 0
backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID))) backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID)))
if err != nil { if err != nil {

View file

@ -10,6 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
@ -38,6 +39,10 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
message, err = u.handleShell(cronjob.Type, cronjob.Name, cronjob.Script) message, err = u.handleShell(cronjob.Type, cronjob.Name, cronjob.Script)
} }
u.HandleRmExpired("LOCAL", "", "", cronjob, nil) u.HandleRmExpired("LOCAL", "", "", cronjob, nil)
case "snapshot":
messageItem := ""
messageItem, record.File, err = u.handleSnapshot(cronjob, record.StartTime)
message = []byte(messageItem)
case "curl": case "curl":
if len(cronjob.URL) == 0 { if len(cronjob.URL) == 0 {
return return
@ -60,6 +65,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
global.LOG.Errorf("cut website log file failed, err: %v", err) global.LOG.Errorf("cut website log file failed, err: %v", err)
} }
} }
if err != nil { if err != nil {
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), string(message)) cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), string(message))
return return
@ -83,7 +89,7 @@ func (u *CronjobService) handleShell(cronType, cornName, script string) ([]byte,
} }
stdout, err := cmd.ExecCronjobWithTimeOut(script, handleDir, 24*time.Hour) stdout, err := cmd.ExecCronjobWithTimeOut(script, handleDir, 24*time.Hour)
if err != nil { if err != nil {
return nil, err return []byte(stdout), err
} }
return []byte(stdout), nil return []byte(stdout), nil
} }
@ -187,6 +193,10 @@ func (u *CronjobService) HandleRmExpired(backType, backupPath, localDir string,
fileItem = strings.TrimPrefix(file, localDir+"/") fileItem = strings.TrimPrefix(file, localDir+"/")
} }
} }
if cronjob.Type == "snapshot" {
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(path.Base(fileItem), ".tar.gz")))
}
_, _ = backClient.Delete(fileItem) _, _ = backClient.Delete(fileItem)
} }
} }
@ -222,7 +232,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
path = sourceDir path = sourceDir
} }
commands := fmt.Sprintf("tar -zcf %s %s %s", targetDir+"/"+name, excludeRules, path) commands := fmt.Sprintf("tar -zcf --warning=no-file-changed --ignore-failed-read %s %s %s", targetDir+"/"+name, excludeRules, path)
global.LOG.Debug(commands) global.LOG.Debug(commands)
stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour)
if err != nil { if err != nil {
@ -550,3 +560,27 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, backup model.Backu
u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, &cronjob, client) u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, &cronjob, client)
return paths, nil return paths, nil
} }
func (u *CronjobService) handleSnapshot(cronjob *model.Cronjob, startTime time.Time) (string, string, error) {
backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID)))
if err != nil {
return "", "", err
}
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
return "", "", err
}
req := dto.SnapshotCreate{
From: backup.Type,
}
message, name, err := NewISnapshotService().HandleSnapshot(true, req, startTime.Format("20060102150405"))
if err != nil {
return message, "", err
}
path := path.Join(strings.TrimPrefix(backup.BackupPath, "/"), "system_snapshot", name+".tar.gz")
u.HandleRmExpired(backup.Type, backup.BackupPath, "", cronjob, client)
return message, path, nil
}

View file

@ -39,6 +39,8 @@ type ISnapshotService interface {
UpdateDescription(req dto.UpdateDescription) error UpdateDescription(req dto.UpdateDescription) error
readFromJson(path string) (SnapshotJson, error) readFromJson(path string) (SnapshotJson, error)
HandleSnapshot(isCronjob bool, req dto.SnapshotCreate, timeNow string) (string, string, error)
} }
func NewISnapshotService() ISnapshotService { func NewISnapshotService() ISnapshotService {
@ -128,103 +130,9 @@ type SnapshotJson struct {
} }
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
localDir, err := loadLocalDir() if _, _, err := u.HandleSnapshot(false, req, time.Now().Format("20060102150405")); err != nil {
if err != nil {
return err return err
} }
var (
snap model.Snapshot
snapStatus model.SnapshotStatus
rootDir string
)
if req.ID == 0 {
timeNow := time.Now().Format("20060102150405")
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
rootDir = path.Join(localDir, fmt.Sprintf("system/1panel_%s_%s", versionItem.Value, timeNow))
snap = model.Snapshot{
Name: fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow),
Description: req.Description,
From: req.From,
Version: versionItem.Value,
Status: constant.StatusWaiting,
}
_ = snapshotRepo.Create(&snap)
snapStatus.SnapID = snap.ID
_ = snapshotRepo.CreateStatus(&snapStatus)
} else {
snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
snapStatus, _ = snapshotRepo.GetStatus(snap.ID)
if snapStatus.ID == 0 {
snapStatus.SnapID = snap.ID
_ = snapshotRepo.CreateStatus(&snapStatus)
}
rootDir = path.Join(localDir, fmt.Sprintf("system/%s", snap.Name))
}
var wg sync.WaitGroup
itemHelper := snapHelper{SnapID: snap.ID, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
backupPanelDir := path.Join(rootDir, "1panel")
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
backupDockerDir := path.Join(rootDir, "docker")
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
jsonItem := SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: localDir,
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
}
if snapStatus.PanelInfo != constant.StatusDone {
wg.Add(1)
go snapJson(itemHelper, snapStatus.ID, jsonItem, rootDir)
}
if snapStatus.Panel != constant.StatusDone {
wg.Add(1)
go snapPanel(itemHelper, snapStatus.ID, backupPanelDir)
}
if snapStatus.PanelCtl != constant.StatusDone {
wg.Add(1)
go snapPanelCtl(itemHelper, snapStatus.ID, backupPanelDir)
}
if snapStatus.PanelService != constant.StatusDone {
wg.Add(1)
go snapPanelService(itemHelper, snapStatus.ID, backupPanelDir)
}
if snapStatus.DaemonJson != constant.StatusDone {
wg.Add(1)
go snapDaemonJson(itemHelper, snapStatus.ID, backupDockerDir)
}
if snapStatus.AppData != constant.StatusDone {
wg.Add(1)
go snapAppData(itemHelper, snapStatus.ID, backupDockerDir)
}
if snapStatus.BackupData != constant.StatusDone {
wg.Add(1)
go snapBackup(itemHelper, snapStatus.ID, localDir, backupPanelDir)
}
if snapStatus.PanelData != constant.StatusDone {
wg.Add(1)
go snapPanelData(itemHelper, snapStatus.ID, localDir, backupPanelDir)
}
go func() {
wg.Wait()
if checkIsAllDone(snap.ID) {
snapCompress(itemHelper, snapStatus.ID, rootDir)
snapUpload(req.From, snapStatus.ID, fmt.Sprintf("%s.tar.gz", rootDir))
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
} else {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
}
}()
return nil return nil
} }
@ -559,6 +467,127 @@ func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) {
return snap, nil return snap, nil
} }
func (u *SnapshotService) HandleSnapshot(isCronjob bool, req dto.SnapshotCreate, timeNow string) (string, string, error) {
localDir, err := loadLocalDir()
if err != nil {
return "", "", err
}
var (
rootDir string
snap model.Snapshot
snapStatus model.SnapshotStatus
)
if req.ID == 0 {
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
name := fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow)
if isCronjob {
name = fmt.Sprintf("snapshot_1panel_%s_%s", versionItem.Value, timeNow)
}
rootDir = path.Join(localDir, "system", name)
snap = model.Snapshot{
Name: name,
Description: req.Description,
From: req.From,
Version: versionItem.Value,
Status: constant.StatusWaiting,
}
_ = snapshotRepo.Create(&snap)
snapStatus.SnapID = snap.ID
_ = snapshotRepo.CreateStatus(&snapStatus)
} else {
snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID))
if err != nil {
return "", "", err
}
snapStatus, _ = snapshotRepo.GetStatus(snap.ID)
if snapStatus.ID == 0 {
snapStatus.SnapID = snap.ID
_ = snapshotRepo.CreateStatus(&snapStatus)
}
rootDir = path.Join(localDir, fmt.Sprintf("system/%s", snap.Name))
}
var wg sync.WaitGroup
itemHelper := snapHelper{SnapID: snap.ID, Status: &snapStatus, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
backupPanelDir := path.Join(rootDir, "1panel")
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
backupDockerDir := path.Join(rootDir, "docker")
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
jsonItem := SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: localDir,
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
}
if snapStatus.PanelInfo != constant.StatusDone {
wg.Add(1)
go snapJson(itemHelper, jsonItem, rootDir)
}
if snapStatus.Panel != constant.StatusDone {
wg.Add(1)
go snapPanel(itemHelper, backupPanelDir)
}
if snapStatus.DaemonJson != constant.StatusDone {
wg.Add(1)
go snapDaemonJson(itemHelper, backupDockerDir)
}
if snapStatus.AppData != constant.StatusDone {
wg.Add(1)
go snapAppData(itemHelper, backupDockerDir)
}
if snapStatus.BackupData != constant.StatusDone {
wg.Add(1)
go snapBackup(itemHelper, localDir, backupPanelDir)
}
if snapStatus.PanelData != constant.StatusDone {
wg.Add(1)
go snapPanelData(itemHelper, localDir, backupPanelDir)
}
if !isCronjob {
go func() {
wg.Wait()
if !checkIsAllDone(snap.ID) {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
snapCompress(itemHelper, rootDir)
if snapStatus.Compress != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir))
if snapStatus.Upload != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
}()
return "", "", nil
}
wg.Wait()
if !checkIsAllDone(snap.ID) {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return loadLogByStatus(snapStatus), snap.Name, fmt.Errorf("snapshot %s backup failed", snap.Name)
}
snapCompress(itemHelper, rootDir)
if snapStatus.Compress != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return loadLogByStatus(snapStatus), snap.Name, fmt.Errorf("snapshot %s compress failed", snap.Name)
}
snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir))
if snapStatus.Upload != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return loadLogByStatus(snapStatus), snap.Name, fmt.Errorf("snapshot %s upload failed", snap.Name)
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
return loadLogByStatus(snapStatus), snap.Name, nil
}
func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error { func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error {
switch operation { switch operation {
case "snapshot": case "snapshot":
@ -644,7 +673,7 @@ func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation strin
if _, err := os.Stat(panelPath); err != nil { if _, err := os.Stat(panelPath); err != nil {
return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err) return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err)
} else { } else {
if err := cpBinary(panelPath, target); err != nil { if err := cpBinary([]string{panelPath}, target); err != nil {
return fmt.Errorf("backup 1panel binary failed, err: %v", err) return fmt.Errorf("backup 1panel binary failed, err: %v", err)
} }
} }
@ -653,7 +682,7 @@ func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation strin
if _, err := os.Stat(source); err != nil { if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err) return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err)
} else { } else {
if err := cpBinary(source, "/usr/local/bin/1panel"); err != nil { if err := cpBinary([]string{source}, "/usr/local/bin/1panel"); err != nil {
return fmt.Errorf("recover 1panel binary failed, err: %v", err) return fmt.Errorf("recover 1panel binary failed, err: %v", err)
} }
} }
@ -668,7 +697,7 @@ func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation st
if _, err := os.Stat(panelctlPath); err != nil { if _, err := os.Stat(panelctlPath); err != nil {
return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err) return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err)
} else { } else {
if err := cpBinary(panelctlPath, target); err != nil { if err := cpBinary([]string{panelctlPath}, target); err != nil {
return fmt.Errorf("backup 1pctl binary failed, err: %v", err) return fmt.Errorf("backup 1pctl binary failed, err: %v", err)
} }
} }
@ -677,7 +706,7 @@ func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation st
if _, err := os.Stat(source); err != nil { if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err) return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err)
} else { } else {
if err := cpBinary(source, "/usr/local/bin/1pctl"); err != nil { if err := cpBinary([]string{source}, "/usr/local/bin/1pctl"); err != nil {
return fmt.Errorf("recover 1pctl binary failed, err: %v", err) return fmt.Errorf("recover 1pctl binary failed, err: %v", err)
} }
} }
@ -692,7 +721,7 @@ func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation stri
if _, err := os.Stat(panelServicePath); err != nil { if _, err := os.Stat(panelServicePath); err != nil {
return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err) return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err)
} else { } else {
if err := cpBinary(panelServicePath, target); err != nil { if err := cpBinary([]string{panelServicePath}, target); err != nil {
return fmt.Errorf("backup 1panel service failed, err: %v", err) return fmt.Errorf("backup 1panel service failed, err: %v", err)
} }
} }
@ -701,7 +730,7 @@ func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation stri
if _, err := os.Stat(source); err != nil { if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel service is not found in snapshot, err: %v", err) return fmt.Errorf("1panel service is not found in snapshot, err: %v", err)
} else { } else {
if err := cpBinary(source, "/etc/systemd/system/1panel.service"); err != nil { if err := cpBinary([]string{source}, "/etc/systemd/system/1panel.service"); err != nil {
return fmt.Errorf("recover 1panel service failed, err: %v", err) return fmt.Errorf("recover 1panel service failed, err: %v", err)
} }
} }
@ -798,10 +827,12 @@ func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
return err return err
} }
for _, snap := range backups { for _, snap := range backups {
itemFile := path.Join(localDir, fmt.Sprintf("system/%s/%s.tar.gz", snap.Name, snap.Name)) itemFile := path.Join(localDir, "system", snap.Name)
if _, err := os.Stat(itemFile); err == nil { _ = os.RemoveAll(itemFile)
_ = os.Remove(itemFile)
} itemTarFile := path.Join(global.CONF.System.TmpDir, "system", snap.Name+".tar.gz")
_ = os.Remove(itemTarFile)
_ = snapshotRepo.DeleteStatus(snap.ID) _ = snapshotRepo.DeleteStatus(snap.ID)
} }
if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
@ -850,8 +881,8 @@ func updateRollbackStatus(id uint, status string, message string) {
} }
} }
func cpBinary(src, dst string) error { func cpBinary(src []string, dst string) error {
stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", src, dst)) stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", strings.Join(src, " "), dst))
if err != nil { if err != nil {
return errors.New(stderr) return errors.New(stderr)
} }
@ -981,12 +1012,6 @@ func checkIsAllDone(snapID uint) bool {
if status.Panel != constant.StatusDone { if status.Panel != constant.StatusDone {
return false return false
} }
if status.PanelCtl != constant.StatusDone {
return false
}
if status.PanelService != constant.StatusDone {
return false
}
if status.PanelInfo != constant.StatusDone { if status.PanelInfo != constant.StatusDone {
return false return false
} }
@ -1004,3 +1029,17 @@ func checkIsAllDone(snapID uint) bool {
} }
return true return true
} }
func loadLogByStatus(status model.SnapshotStatus) string {
logs := ""
logs += fmt.Sprintf("Write 1Panel basic information: %s \n", status.PanelInfo)
logs += fmt.Sprintf("Backup 1Panel system files: %s \n", status.Panel)
logs += fmt.Sprintf("Backup Docker configuration file: %s \n", status.DaemonJson)
logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppData)
logs += fmt.Sprintf("Backup 1Panel data directory: %s \n", status.PanelData)
logs += fmt.Sprintf("Backup local backup directory for 1Panel: %s \n", status.BackupData)
logs += fmt.Sprintf("Create snapshot file: %s \n", status.BackupData)
logs += fmt.Sprintf("Upload snapshot file: %s \n", status.BackupData)
return logs
}

View file

@ -11,6 +11,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@ -19,76 +20,64 @@ import (
type snapHelper struct { type snapHelper struct {
SnapID uint SnapID uint
Status *model.SnapshotStatus
Ctx context.Context Ctx context.Context
FileOp files.FileOp FileOp files.FileOp
Wg *sync.WaitGroup Wg *sync.WaitGroup
} }
func snapJson(snap snapHelper, statusID uint, snapJson SnapshotJson, targetDir string) { func snapJson(snap snapHelper, snapJson SnapshotJson, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": constant.Running})
status := constant.StatusDone status := constant.StatusDone
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t")
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil { if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil {
status = err.Error() status = err.Error()
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": status}) snap.Status.PanelInfo = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status})
} }
func snapPanel(snap snapHelper, statusID uint, targetDir string) { func snapPanel(snap snapHelper, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
status := constant.StatusDone status := constant.StatusDone
if err := cpBinary("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { if err := cpBinary([]string{"/usr/local/bin/1panel", "/usr/local/bin/1pctl", "/etc/systemd/system/1panel.service"}, targetDir); err != nil {
status = err.Error() status = err.Error()
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": status}) snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
} }
func snapPanelCtl(snap snapHelper, statusID uint, targetDir string) { func snapDaemonJson(snap snapHelper, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": constant.Running})
status := constant.StatusDone status := constant.StatusDone
if err := cpBinary("/usr/local/bin/1pctl", path.Join(targetDir, "1pctl")); err != nil {
status = err.Error()
}
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": status})
}
func snapPanelService(snap snapHelper, statusID uint, targetDir string) {
defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": constant.Running})
status := constant.StatusDone
if err := cpBinary("/etc/systemd/system/1panel.service", path.Join(targetDir, "1panel.service")); err != nil {
status = err.Error()
}
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": status})
}
func snapDaemonJson(snap snapHelper, statusID uint, targetDir string) {
defer snap.Wg.Done()
if !snap.FileOp.Stat("/etc/docker/daemon.json") { if !snap.FileOp.Stat("/etc/docker/daemon.json") {
snap.Status.DaemonJson = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
return return
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running})
status := constant.StatusDone if err := cpBinary([]string{"/etc/docker/daemon.json"}, path.Join(targetDir, "daemon.json")); err != nil {
if err := cpBinary("/etc/docker/daemon.json", path.Join(targetDir, "daemon.json")); err != nil {
status = err.Error() status = err.Error()
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": status}) snap.Status.DaemonJson = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
} }
func snapAppData(snap snapHelper, statusID uint, targetDir string) { func snapAppData(snap snapHelper, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running})
appInstalls, err := appInstallRepo.ListBy() appInstalls, err := appInstallRepo.ListBy()
if err != nil { if err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()}) snap.Status.AppData = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
return return
} }
runtimes, err := runtimeRepo.List() runtimes, err := runtimeRepo.List()
if err != nil { if err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()}) snap.Status.AppData = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
return return
} }
imageRegex := regexp.MustCompile(`image:\s*(.*)`) imageRegex := regexp.MustCompile(`image:\s*(.*)`)
@ -119,28 +108,31 @@ func snapAppData(snap snapHelper, statusID uint, targetDir string) {
global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
if err != nil { if err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": std}) snap.Status.AppData = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std})
return return
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.StatusDone}) snap.Status.AppData = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone})
} }
func snapBackup(snap snapHelper, statusID uint, localDir, targetDir string) { func snapBackup(snap snapHelper, localDir, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": constant.Running})
status := constant.StatusDone status := constant.StatusDone
if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;"); err != nil { if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;"); err != nil {
status = err.Error() status = err.Error()
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": status}) snap.Status.BackupData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status})
} }
func snapPanelData(snap snapHelper, statusID uint, localDir, targetDir string) { func snapPanelData(snap snapHelper, localDir, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_data": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": constant.Running})
status := constant.StatusDone status := constant.StatusDone
dataDir := path.Join(global.CONF.System.BaseDir, "1panel") dataDir := path.Join(global.CONF.System.BaseDir, "1panel")
exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;" exclusionRules := "./tmp;./log;./cache;"
if strings.Contains(localDir, dataDir) { if strings.Contains(localDir, dataDir) {
exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";") exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";")
} }
@ -150,48 +142,57 @@ func snapPanelData(snap snapHelper, statusID uint, localDir, targetDir string) {
status = err.Error() status = err.Error()
} }
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting}) _ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting})
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_data": status})
snap.Status.PanelData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status})
} }
func snapCompress(snap snapHelper, statusID uint, rootDir string) { func snapCompress(snap snapHelper, rootDir string) {
defer func() { defer func() {
global.LOG.Debugf("remove snapshot file %s", rootDir) global.LOG.Debugf("remove snapshot file %s", rootDir)
_ = os.RemoveAll(rootDir) _ = os.RemoveAll(rootDir)
}() }()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusRunning}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusRunning})
tmpDir := path.Join(global.CONF.System.TmpDir, "system") tmpDir := path.Join(global.CONF.System.TmpDir, "system")
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir)) fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir))
if err := snap.FileOp.Compress([]string{rootDir}, tmpDir, fileName, files.TarGz); err != nil { if err := snap.FileOp.Compress([]string{rootDir}, tmpDir, fileName, files.TarGz); err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": err.Error()}) snap.Status.Compress = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()})
return return
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusDone})
snap.Status.Compress = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusDone})
} }
func snapUpload(account string, statusID uint, file string) { func snapUpload(snap snapHelper, account string, file string) {
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file)) source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file))
defer func() { defer func() {
global.LOG.Debugf("remove snapshot file %s", source) global.LOG.Debugf("remove snapshot file %s", source)
_ = os.Remove(source) _ = os.Remove(source)
}() }()
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusUploading}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusUploading})
backup, err := backupRepo.Get(commonRepo.WithByType(account)) backup, err := backupRepo.Get(commonRepo.WithByType(account))
if err != nil { if err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) snap.Status.Upload = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
return return
} }
client, err := NewIBackupService().NewClient(&backup) client, err := NewIBackupService().NewClient(&backup)
if err != nil { if err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) snap.Status.Upload = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
return return
} }
target := path.Join(backup.BackupPath, "system_snapshot", path.Base(file)) target := path.Join(backup.BackupPath, "system_snapshot", path.Base(file))
if _, err := client.Upload(source, target); err != nil { if _, err := client.Upload(source, target); err != nil {
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) snap.Status.Upload = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
return return
} }
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusDone}) snap.Status.Upload = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone})
} }
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error { func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
@ -211,7 +212,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
exStr += exclude exStr += exclude
} }
commands := fmt.Sprintf("tar --warning=no-file-changed -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir) commands := fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir)
global.LOG.Debug(commands) global.LOG.Debug(commands)
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
if err != nil { if err != nil {

View file

@ -127,13 +127,13 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
} }
global.LOG.Info("backup original data successful, now start to upgrade!") global.LOG.Info("backup original data successful, now start to upgrade!")
if err := cpBinary(tmpDir+"/1panel", "/usr/local/bin/1panel"); err != nil { if err := cpBinary([]string{tmpDir + "/1panel"}, "/usr/local/bin/1panel"); err != nil {
u.handleRollback(fileOp, originalDir, 1) u.handleRollback(fileOp, originalDir, 1)
global.LOG.Errorf("upgrade 1panel failed, err: %v", err) global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
return return
} }
if err := cpBinary(tmpDir+"/1pctl", "/usr/local/bin/1pctl"); err != nil { if err := cpBinary([]string{tmpDir + "/1pctl"}, "/usr/local/bin/1pctl"); err != nil {
u.handleRollback(fileOp, originalDir, 2) u.handleRollback(fileOp, originalDir, 2)
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err) global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
return return
@ -144,7 +144,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
return return
} }
if err := cpBinary(tmpDir+"/1panel.service", "/etc/systemd/system/1panel.service"); err != nil { if err := cpBinary([]string{tmpDir + "/1panel.service"}, "/etc/systemd/system/1panel.service"); err != nil {
u.handleRollback(fileOp, originalDir, 3) u.handleRollback(fileOp, originalDir, 3)
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err) global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err)
return return
@ -179,22 +179,22 @@ func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) e
func (u *UpgradeService) handleRollback(fileOp files.FileOp, originalDir string, errStep int) { func (u *UpgradeService) handleRollback(fileOp files.FileOp, originalDir string, errStep int) {
dbPath := global.CONF.System.DbPath + "/1Panel.db" dbPath := global.CONF.System.DbPath + "/1Panel.db"
_ = settingRepo.Update("SystemStatus", "Free") _ = settingRepo.Update("SystemStatus", "Free")
if err := cpBinary(originalDir+"/1Panel.db", dbPath); err != nil { if err := cpBinary([]string{originalDir + "/1Panel.db"}, dbPath); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err) global.LOG.Errorf("rollback 1panel failed, err: %v", err)
} }
if err := cpBinary(originalDir+"/1panel", "/usr/local/bin/1panel"); err != nil { if err := cpBinary([]string{originalDir + "/1panel"}, "/usr/local/bin/1panel"); err != nil {
global.LOG.Errorf("rollback 1pctl failed, err: %v", err) global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
} }
if errStep == 1 { if errStep == 1 {
return return
} }
if err := cpBinary(originalDir+"/1pctl", "/usr/local/bin/1pctl"); err != nil { if err := cpBinary([]string{originalDir + "/1pctl"}, "/usr/local/bin/1pctl"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err) global.LOG.Errorf("rollback 1panel failed, err: %v", err)
} }
if errStep == 2 { if errStep == 2 {
return return
} }
if err := cpBinary(originalDir+"/1panel.service", "/etc/systemd/system/1panel.service"); err != nil { if err := cpBinary([]string{originalDir + "/1panel.service"}, "/etc/systemd/system/1panel.service"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err) global.LOG.Errorf("rollback 1panel failed, err: %v", err)
} }
} }

View file

@ -86,12 +86,6 @@ func handleSnapStatus() {
if statu.Panel == constant.StatusRunning { if statu.Panel == constant.StatusRunning {
updatas["panel"] = constant.StatusFailed updatas["panel"] = constant.StatusFailed
} }
if statu.PanelCtl == constant.StatusRunning {
updatas["panel_ctl"] = constant.StatusFailed
}
if statu.PanelService == constant.StatusRunning {
updatas["panel_service"] = constant.StatusFailed
}
if statu.PanelInfo == constant.StatusRunning { if statu.PanelInfo == constant.StatusRunning {
updatas["panel_info"] = constant.StatusFailed updatas["panel_info"] = constant.StatusFailed
} }

View file

@ -109,8 +109,6 @@ export namespace Setting {
} }
export interface SnapshotStatus { export interface SnapshotStatus {
panel: string; panel: string;
panelCtl: string;
panelService: string;
panelInfo: string; panelInfo: string;
daemonJson: string; daemonJson: string;
appData: string; appData: string;

View file

@ -709,6 +709,7 @@ const message = {
'This operation records all job execution records, backup files, and log files. Do you want to continue?', 'This operation records all job execution records, backup files, and log files. Do you want to continue?',
directory: 'Backup directory', directory: 'Backup directory',
sourceDir: 'Backup directory', sourceDir: 'Backup directory',
snapshot: 'System Snapshot',
allOptionHelper: allOptionHelper:
'The current task plan is to back up all [{0}]. Direct download is not supported at the moment. You can check the backup list of [{0}] menu.', 'The current task plan is to back up all [{0}]. Direct download is not supported at the moment. You can check the backup list of [{0}] menu.',
exclusionRules: 'Exclusive rule', exclusionRules: 'Exclusive rule',
@ -1110,15 +1111,13 @@ const message = {
snapshot: 'Snapshot', snapshot: 'Snapshot',
status: 'Snapshot status', status: 'Snapshot status',
panelBin: 'Backup 1Panel binary', panelInfo: 'Write 1Panel basic information',
panelCtl: 'Backup 1Panel script', panelBin: 'Backup 1Panel system files',
panelService: 'Backup 1Panel service', daemonJson: 'Backup Docker configuration file',
panelInfo: 'Backup 1Panel basic information', appData: 'Backup installed apps from 1Panel',
daemonJson: 'Backup Docker daemon.json',
appData: 'Backup 1Panel application',
panelData: 'Backup 1Panel data directory', panelData: 'Backup 1Panel data directory',
backupData: 'Backup 1Panel local backup directory', backupData: 'Backup local backup directory for 1Panel',
compress: 'Compress snapshot file', compress: 'Create snapshot file',
upload: 'Upload snapshot file', upload: 'Upload snapshot file',
thirdPartySupport: 'Only third-party accounts are supported', thirdPartySupport: 'Only third-party accounts are supported',
recoverDetail: 'Recover detail', recoverDetail: 'Recover detail',

View file

@ -680,6 +680,7 @@ const message = {
cleanHelper: '該操作將所有任務執行記錄備份文件和日誌文件是否繼續', cleanHelper: '該操作將所有任務執行記錄備份文件和日誌文件是否繼續',
directory: '備份目錄', directory: '備份目錄',
sourceDir: '備份目錄', sourceDir: '備份目錄',
snapshot: '系統快照',
allOptionHelper: '當前計劃任務為備份所有{0}暫不支持直接下載可在{0}備份列表中查看', allOptionHelper: '當前計劃任務為備份所有{0}暫不支持直接下載可在{0}備份列表中查看',
exclusionRules: '排除規則', exclusionRules: '排除規則',
saveLocal: '同時保留本地備份和雲存儲保留份數一致', saveLocal: '同時保留本地備份和雲存儲保留份數一致',
@ -1001,15 +1002,13 @@ const message = {
snapshot: '快照', snapshot: '快照',
status: '快照狀態', status: '快照狀態',
panelBin: '備份 1Panel 二進製', panelInfo: '寫入 1Panel 基礎信息',
panelCtl: '備份 1Panel 腳本', panelBin: '備份 1Panel 系統文件',
panelService: '備份 1Panel 服務', daemonJson: '備份 Docker 配置文件',
panelInfo: '備份 1Panel 基礎信息', appData: '備份 1Panel 已安裝應用',
daemonJson: '備份 Docker 配置',
appData: '備份 1Panel 應用',
panelData: '備份 1Panel 數據目錄', panelData: '備份 1Panel 數據目錄',
backupData: '備份 1Panel 本地備份目錄', backupData: '備份 1Panel 本地備份目錄',
compress: '壓縮快照文件', compress: '製作快照文件',
upload: '上傳快照文件', upload: '上傳快照文件',
thirdPartySupport: '僅支持第三方賬號', thirdPartySupport: '僅支持第三方賬號',
recoverDetail: '恢復詳情', recoverDetail: '恢復詳情',

View file

@ -680,6 +680,7 @@ const message = {
cleanHelper: '该操作将所有任务执行记录备份文件和日志文件是否继续', cleanHelper: '该操作将所有任务执行记录备份文件和日志文件是否继续',
directory: '备份目录', directory: '备份目录',
sourceDir: '备份目录', sourceDir: '备份目录',
snapshot: '系统快照',
allOptionHelper: '当前计划任务为备份所有{0}暂不支持直接下载可在{0}备份列表中查看', allOptionHelper: '当前计划任务为备份所有{0}暂不支持直接下载可在{0}备份列表中查看',
exclusionRules: '排除规则', exclusionRules: '排除规则',
saveLocal: '同时保留本地备份和云存储保留份数一致', saveLocal: '同时保留本地备份和云存储保留份数一致',
@ -1001,15 +1002,13 @@ const message = {
snapshot: '快照', snapshot: '快照',
status: '快照状态', status: '快照状态',
panelBin: '备份 1Panel 二进制', panelInfo: '写入 1Panel 基础信息',
panelCtl: '备份 1Panel 脚本', panelBin: '备份 1Panel 系统文件',
panelService: '备份 1Panel 服务', daemonJson: '备份 Docker 配置文件',
panelInfo: '备份 1Panel 基础信息', appData: '备份 1Panel 已安装应用',
daemonJson: '备份 Docker 配置',
appData: '备份 1Panel 应用',
panelData: '备份 1Panel 数据目录', panelData: '备份 1Panel 数据目录',
backupData: '备份 1Panel 本地备份目录', backupData: '备份 1Panel 本地备份目录',
compress: '压缩快照文件', compress: '制作快照文件',
upload: '上传快照文件', upload: '上传快照文件',
thirdPartySupport: '仅支持第三方账号', thirdPartySupport: '仅支持第三方账号',
recoverDetail: '恢复详情', recoverDetail: '恢复详情',

View file

@ -23,6 +23,7 @@
<el-option value="website" :label="$t('cronjob.website')" /> <el-option value="website" :label="$t('cronjob.website')" />
<el-option value="database" :label="$t('cronjob.database')" /> <el-option value="database" :label="$t('cronjob.database')" />
<el-option value="directory" :label="$t('cronjob.directory')" /> <el-option value="directory" :label="$t('cronjob.directory')" />
<el-option value="snapshot" :label="$t('cronjob.snapshot')" />
<el-option value="curl" :label="$t('cronjob.curl')" /> <el-option value="curl" :label="$t('cronjob.curl')" />
<el-option value="ntp" :label="$t('cronjob.ntp')" /> <el-option value="ntp" :label="$t('cronjob.ntp')" />
<el-option value="cutWebsiteLog" :label="$t('cronjob.cutWebsiteLog')" /> <el-option value="cutWebsiteLog" :label="$t('cronjob.cutWebsiteLog')" />
@ -165,12 +166,13 @@
<div v-if="isBackup()"> <div v-if="isBackup()">
<el-form-item :label="$t('cronjob.target')" prop="targetDirID"> <el-form-item :label="$t('cronjob.target')" prop="targetDirID">
<el-select class="selectClass" v-model="dialogData.rowData!.targetDirID"> <el-select class="selectClass" v-model="dialogData.rowData!.targetDirID">
<el-option <div v-for="item in backupOptions" :key="item.label">
v-for="item in backupOptions" <el-option
:key="item.label" v-if="item.label !== $t('setting.LOCAL') || dialogData.rowData!.type !== 'snapshot'"
:value="item.value" :value="item.value"
:label="item.label" :label="item.label"
/> />
</div>
</el-select> </el-select>
<span class="input-help"> <span class="input-help">
{{ $t('cronjob.targetHelper') }} {{ $t('cronjob.targetHelper') }}
@ -184,7 +186,9 @@
</el-link> </el-link>
</span> </span>
</el-form-item> </el-form-item>
<el-form-item v-if="dialogData.rowData!.targetDirID !== localDirID"> <el-form-item
v-if="dialogData.rowData!.targetDirID !== localDirID && dialogData.rowData!.type !== 'snapshot'"
>
<el-checkbox v-model="dialogData.rowData!.keepLocal"> <el-checkbox v-model="dialogData.rowData!.keepLocal">
{{ $t('cronjob.saveLocal') }} {{ $t('cronjob.saveLocal') }}
</el-checkbox> </el-checkbox>
@ -448,6 +452,20 @@ const changeType = () => {
dialogData.value.rowData.hour = 1; dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30; dialogData.value.rowData.minute = 30;
break; break;
case 'snapshot':
dialogData.value.rowData.specType = 'perWeek';
dialogData.value.rowData.week = 1;
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
dialogData.value.rowData.keepLocal = false;
dialogData.value.rowData.targetDirID = null;
for (const item of backupOptions.value) {
if (item.label !== i18n.global.t('setting.LOCAL')) {
dialogData.value.rowData.targetDirID = item.value;
break;
}
}
break;
case 'directory': case 'directory':
dialogData.value.rowData.specType = 'perDay'; dialogData.value.rowData.specType = 'perDay';
dialogData.value.rowData.hour = 1; dialogData.value.rowData.hour = 1;
@ -504,7 +522,8 @@ function isBackup() {
dialogData.value.rowData!.type === 'app' || dialogData.value.rowData!.type === 'app' ||
dialogData.value.rowData!.type === 'website' || dialogData.value.rowData!.type === 'website' ||
dialogData.value.rowData!.type === 'database' || dialogData.value.rowData!.type === 'database' ||
dialogData.value.rowData!.type === 'directory' dialogData.value.rowData!.type === 'directory' ||
dialogData.value.rowData!.type === 'snapshot'
); );
} }

View file

@ -167,7 +167,7 @@
</template> </template>
<span class="status-count">{{ dialogData.rowData!.targetDir }}</span> <span class="status-count">{{ dialogData.rowData!.targetDir }}</span>
<el-button <el-button
v-if="currentRecord?.status === 'Success'" v-if="currentRecord?.status === 'Success' && dialogData.rowData!.type !== 'snapshot'"
type="primary" type="primary"
style="margin-left: 10px" style="margin-left: 10px"
link link
@ -238,6 +238,10 @@
</template> </template>
<span class="status-count">{{ dialogData.rowData!.retainCopies }}</span> <span class="status-count">{{ dialogData.rowData!.retainCopies }}</span>
</el-form-item> </el-form-item>
<el-form-item
class="description"
v-if="dialogData.rowData!.type === 'snapshot'"
></el-form-item>
</el-row> </el-row>
<el-form-item class="description" v-if=" dialogData.rowData!.type === 'directory'"> <el-form-item class="description" v-if=" dialogData.rowData!.type === 'directory'">
<template #label> <template #label>
@ -678,7 +682,8 @@ function isBackup() {
dialogData.value.rowData!.type === 'app' || dialogData.value.rowData!.type === 'app' ||
dialogData.value.rowData!.type === 'website' || dialogData.value.rowData!.type === 'website' ||
dialogData.value.rowData!.type === 'database' || dialogData.value.rowData!.type === 'database' ||
dialogData.value.rowData!.type === 'directory' dialogData.value.rowData!.type === 'directory' ||
dialogData.value.rowData!.type === 'snapshot'
); );
} }
function loadWeek(i: number) { function loadWeek(i: number) {

View file

@ -57,7 +57,7 @@
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status"> <el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
<template #default="{ row }"> <template #default="{ row }">
<el-button <el-button
v-if="row.status === 'Waiting' || row.status === 'onSaveData'" v-if="row.status === 'Waiting' || row.status === 'OnSaveData'"
type="primary" type="primary"
@click="onLoadStatus(row)" @click="onLoadStatus(row)"
link link

View file

@ -12,30 +12,6 @@
</div> </div>
</template> </template>
<div v-loading="loading"> <div v-loading="loading">
<el-alert :type="loadStatus(status.panel)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.panel)" link>{{ $t('setting.panelBin') }}</el-button>
<div v-if="showErrorMsg(status.panel)" class="top-margin">
<span class="err-message">{{ status.panel }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.panelCtl)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.panelCtl)" link>{{ $t('setting.panelCtl') }}</el-button>
<div v-if="showErrorMsg(status.panelCtl)" class="top-margin">
<span class="err-message">{{ status.panelCtl }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.panelService)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.panelService)" link>{{ $t('setting.panelService') }}</el-button>
<div v-if="showErrorMsg(status.panelService)" class="top-margin">
<span class="err-message">{{ status.panelService }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.panelInfo)" :closable="false"> <el-alert :type="loadStatus(status.panelInfo)" :closable="false">
<template #title> <template #title>
<el-button :icon="loadIcon(status.panelInfo)" link>{{ $t('setting.panelInfo') }}</el-button> <el-button :icon="loadIcon(status.panelInfo)" link>{{ $t('setting.panelInfo') }}</el-button>
@ -44,6 +20,14 @@
</div> </div>
</template> </template>
</el-alert> </el-alert>
<el-alert :type="loadStatus(status.panel)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.panel)" link>{{ $t('setting.panelBin') }}</el-button>
<div v-if="showErrorMsg(status.panel)" class="top-margin">
<span class="err-message">{{ status.panel }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.daemonJson)" :closable="false"> <el-alert :type="loadStatus(status.daemonJson)" :closable="false">
<template #title> <template #title>
<el-button :icon="loadIcon(status.daemonJson)" link>{{ $t('setting.daemonJson') }}</el-button> <el-button :icon="loadIcon(status.daemonJson)" link>{{ $t('setting.daemonJson') }}</el-button>
@ -113,8 +97,6 @@ import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
const status = reactive<Setting.SnapshotStatus>({ const status = reactive<Setting.SnapshotStatus>({
panel: '', panel: '',
panelCtl: '',
panelService: '',
panelInfo: '', panelInfo: '',
daemonJson: '', daemonJson: '',
appData: '', appData: '',
@ -158,8 +140,6 @@ const loadCurrentStatus = async () => {
.then((res) => { .then((res) => {
loading.value = false; loading.value = false;
status.panel = res.data.panel; status.panel = res.data.panel;
status.panelCtl = res.data.panelCtl;
status.panelService = res.data.panelService;
status.panelInfo = res.data.panelInfo; status.panelInfo = res.data.panelInfo;
status.daemonJson = res.data.daemonJson; status.daemonJson = res.data.daemonJson;
status.appData = res.data.appData; status.appData = res.data.appData;
@ -196,8 +176,6 @@ const onWatch = () => {
if (keepLoadStatus()) { if (keepLoadStatus()) {
const res = await loadSnapStatus(snapID.value); const res = await loadSnapStatus(snapID.value);
status.panel = res.data.panel; status.panel = res.data.panel;
status.panelCtl = res.data.panelCtl;
status.panelService = res.data.panelService;
status.panelInfo = res.data.panelInfo; status.panelInfo = res.data.panelInfo;
status.daemonJson = res.data.daemonJson; status.daemonJson = res.data.daemonJson;
status.appData = res.data.appData; status.appData = res.data.appData;
@ -214,12 +192,6 @@ const keepLoadStatus = () => {
if (status.panel === 'Running') { if (status.panel === 'Running') {
return true; return true;
} }
if (status.panelCtl === 'Running') {
return true;
}
if (status.panelService === 'Running') {
return true;
}
if (status.panelInfo === 'Running') { if (status.panelInfo === 'Running') {
return true; return true;
} }
@ -255,12 +227,6 @@ const showRetry = () => {
if (status.panel !== 'Running' && status.panel !== 'Done') { if (status.panel !== 'Running' && status.panel !== 'Done') {
return true; return true;
} }
if (status.panelCtl !== 'Running' && status.panelCtl !== 'Done') {
return true;
}
if (status.panelService !== 'Running' && status.panelService !== 'Done') {
return true;
}
if (status.panelInfo !== 'Running' && status.panelInfo !== 'Done') { if (status.panelInfo !== 'Running' && status.panelInfo !== 'Done') {
return true; return true;
} }