diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 7f7181900..6db7c01bb 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -76,14 +76,12 @@ type PortUpdate struct { } type SnapshotStatus struct { - Panel string `json:"panel"` - PanelCtl string `json:"panelCtl"` - PanelService string `json:"panelService"` - PanelInfo string `json:"panelInfo"` - DaemonJson string `json:"daemonJson"` - AppData string `json:"appData"` - PanelData string `json:"panelData"` - BackupData string `json:"backupData"` + Panel string `json:"panel"` + PanelInfo string `json:"panelInfo"` + DaemonJson string `json:"daemonJson"` + AppData string `json:"appData"` + PanelData string `json:"panelData"` + BackupData string `json:"backupData"` Compress string `json:"compress"` Upload string `json:"upload"` diff --git a/backend/app/model/snapshot.go b/backend/app/model/snapshot.go index c56c9cfdb..006bf3207 100644 --- a/backend/app/model/snapshot.go +++ b/backend/app/model/snapshot.go @@ -20,15 +20,13 @@ type Snapshot struct { type SnapshotStatus struct { BaseModel - SnapID uint `gorm:"type:decimal" json:"snapID"` - Panel string `json:"panel" gorm:"type:varchar(64);default:Running"` - PanelCtl string `json:"panelCtl" gorm:"type:varchar(64);default:Running"` - PanelService string `json:"panelService" gorm:"type:varchar(64);default:Running"` - PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"` - DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"` - AppData string `json:"appData" 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"` + SnapID uint `gorm:"type:decimal" json:"snapID"` + Panel string `json:"panel" gorm:"type:varchar(64);default:Running"` + PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"` + DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"` + AppData string `json:"appData" 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"` Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"` diff --git a/backend/app/service/cornjob.go b/backend/app/service/cornjob.go index 6e9ca4d37..acc9c9f2f 100644 --- a/backend/app/service/cornjob.go +++ b/backend/app/service/cornjob.go @@ -46,7 +46,7 @@ func (u *CronjobService) SearchWithPage(search dto.SearchWithPage) (int64, inter if err := copier.Copy(&item, &cronjob); err != nil { 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))) if len(backup.Type) != 0 { item.TargetDir = backup.Type @@ -103,7 +103,7 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error { if err != nil { 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 backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID))) if err != nil { diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index b1b81fec3..cc63261c6 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" @@ -38,6 +39,10 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { message, err = u.handleShell(cronjob.Type, cronjob.Name, cronjob.Script) } u.HandleRmExpired("LOCAL", "", "", cronjob, nil) + case "snapshot": + messageItem := "" + messageItem, record.File, err = u.handleSnapshot(cronjob, record.StartTime) + message = []byte(messageItem) case "curl": if len(cronjob.URL) == 0 { return @@ -60,6 +65,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { global.LOG.Errorf("cut website log file failed, err: %v", err) } } + if err != nil { cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), string(message)) return @@ -83,7 +89,7 @@ func (u *CronjobService) handleShell(cronType, cornName, script string) ([]byte, } stdout, err := cmd.ExecCronjobWithTimeOut(script, handleDir, 24*time.Hour) if err != nil { - return nil, err + return []byte(stdout), err } return []byte(stdout), nil } @@ -187,6 +193,10 @@ func (u *CronjobService) HandleRmExpired(backType, backupPath, localDir string, fileItem = strings.TrimPrefix(file, localDir+"/") } } + + if cronjob.Type == "snapshot" { + _ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(path.Base(fileItem), ".tar.gz"))) + } _, _ = backClient.Delete(fileItem) } } @@ -222,7 +232,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error { 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) stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) 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) 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 +} diff --git a/backend/app/service/snapshot.go b/backend/app/service/snapshot.go index 82ad97bf7..cf05d337f 100644 --- a/backend/app/service/snapshot.go +++ b/backend/app/service/snapshot.go @@ -39,6 +39,8 @@ type ISnapshotService interface { UpdateDescription(req dto.UpdateDescription) error readFromJson(path string) (SnapshotJson, error) + + HandleSnapshot(isCronjob bool, req dto.SnapshotCreate, timeNow string) (string, string, error) } func NewISnapshotService() ISnapshotService { @@ -128,103 +130,9 @@ type SnapshotJson struct { } func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { - localDir, err := loadLocalDir() - if err != nil { + if _, _, err := u.HandleSnapshot(false, req, time.Now().Format("20060102150405")); err != nil { 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 } @@ -559,6 +467,127 @@ func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) { 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 { switch operation { case "snapshot": @@ -644,7 +673,7 @@ func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation strin if _, err := os.Stat(panelPath); err != nil { return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err) } 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) } } @@ -653,7 +682,7 @@ func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation strin if _, err := os.Stat(source); err != nil { return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err) } 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) } } @@ -668,7 +697,7 @@ func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation st if _, err := os.Stat(panelctlPath); err != nil { return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err) } 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) } } @@ -677,7 +706,7 @@ func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation st if _, err := os.Stat(source); err != nil { return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err) } 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) } } @@ -692,7 +721,7 @@ func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation stri if _, err := os.Stat(panelServicePath); err != nil { return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err) } 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) } } @@ -701,7 +730,7 @@ func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation stri if _, err := os.Stat(source); err != nil { return fmt.Errorf("1panel service is not found in snapshot, err: %v", err) } 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) } } @@ -798,10 +827,12 @@ func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error { return err } for _, snap := range backups { - itemFile := path.Join(localDir, fmt.Sprintf("system/%s/%s.tar.gz", snap.Name, snap.Name)) - if _, err := os.Stat(itemFile); err == nil { - _ = os.Remove(itemFile) - } + itemFile := path.Join(localDir, "system", snap.Name) + _ = os.RemoveAll(itemFile) + + itemTarFile := path.Join(global.CONF.System.TmpDir, "system", snap.Name+".tar.gz") + _ = os.Remove(itemTarFile) + _ = snapshotRepo.DeleteStatus(snap.ID) } 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 { - stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", src, dst)) +func cpBinary(src []string, dst string) error { + stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", strings.Join(src, " "), dst)) if err != nil { return errors.New(stderr) } @@ -981,12 +1012,6 @@ func checkIsAllDone(snapID uint) bool { if status.Panel != constant.StatusDone { return false } - if status.PanelCtl != constant.StatusDone { - return false - } - if status.PanelService != constant.StatusDone { - return false - } if status.PanelInfo != constant.StatusDone { return false } @@ -1004,3 +1029,17 @@ func checkIsAllDone(snapID uint) bool { } 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 +} diff --git a/backend/app/service/snapshot_create.go b/backend/app/service/snapshot_create.go index a11c0cb94..8c3d87e96 100644 --- a/backend/app/service/snapshot_create.go +++ b/backend/app/service/snapshot_create.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/cmd" @@ -19,76 +20,64 @@ import ( type snapHelper struct { SnapID uint + Status *model.SnapshotStatus Ctx context.Context FileOp files.FileOp 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() - _ = 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 remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil { 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() - _ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": constant.Running}) + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running}) 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() } - _ = 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() - _ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": constant.Running}) 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") { + snap.Status.DaemonJson = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status}) return } - _ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": constant.Running}) - status := constant.StatusDone - if err := cpBinary("/etc/docker/daemon.json", path.Join(targetDir, "daemon.json")); err != nil { + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running}) + if err := cpBinary([]string{"/etc/docker/daemon.json"}, path.Join(targetDir, "daemon.json")); err != nil { 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() - _ = 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() 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 } runtimes, err := runtimeRepo.List() 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 } 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")) std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) 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 } - _ = 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() - _ = 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 if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;"); err != nil { 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() - _ = 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 dataDir := path.Join(global.CONF.System.BaseDir, "1panel") - exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;" + exclusionRules := "./tmp;./log;./cache;" if strings.Contains(localDir, dataDir) { exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";") } @@ -150,48 +142,57 @@ func snapPanelData(snap snapHelper, statusID uint, localDir, targetDir string) { status = err.Error() } _ = 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() { global.LOG.Debugf("remove snapshot file %s", 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") fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir)) 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 } - _ = 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)) defer func() { global.LOG.Debugf("remove snapshot file %s", 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)) 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 } client, err := NewIBackupService().NewClient(&backup) 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 } target := path.Join(backup.BackupPath, "system_snapshot", path.Base(file)) 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 } - _ = 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 { @@ -211,7 +212,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error { 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) stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) if err != nil { diff --git a/backend/app/service/upgrade.go b/backend/app/service/upgrade.go index 0ea5b3820..e400b542f 100644 --- a/backend/app/service/upgrade.go +++ b/backend/app/service/upgrade.go @@ -127,13 +127,13 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error { } 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) global.LOG.Errorf("upgrade 1panel failed, err: %v", err) 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) global.LOG.Errorf("upgrade 1pctl failed, err: %v", err) return @@ -144,7 +144,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error { 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) global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err) 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) { dbPath := global.CONF.System.DbPath + "/1Panel.db" _ = 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) } - 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) } if errStep == 1 { 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) } if errStep == 2 { 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) } } diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index 0f2c3f990..aeaa40615 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -86,12 +86,6 @@ func handleSnapStatus() { if statu.Panel == constant.StatusRunning { 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 { updatas["panel_info"] = constant.StatusFailed } diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 3dc388b18..e295ee868 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -109,8 +109,6 @@ export namespace Setting { } export interface SnapshotStatus { panel: string; - panelCtl: string; - panelService: string; panelInfo: string; daemonJson: string; appData: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 04528c761..e16840db2 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -709,6 +709,7 @@ const message = { 'This operation records all job execution records, backup files, and log files. Do you want to continue?', directory: 'Backup directory', sourceDir: 'Backup directory', + snapshot: 'System Snapshot', 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.', exclusionRules: 'Exclusive rule', @@ -1110,15 +1111,13 @@ const message = { snapshot: 'Snapshot', status: 'Snapshot status', - panelBin: 'Backup 1Panel binary', - panelCtl: 'Backup 1Panel script', - panelService: 'Backup 1Panel service', - panelInfo: 'Backup 1Panel basic information', - daemonJson: 'Backup Docker daemon.json', - appData: 'Backup 1Panel application', + panelInfo: 'Write 1Panel basic information', + panelBin: 'Backup 1Panel system files', + daemonJson: 'Backup Docker configuration file', + appData: 'Backup installed apps from 1Panel', panelData: 'Backup 1Panel data directory', - backupData: 'Backup 1Panel local backup directory', - compress: 'Compress snapshot file', + backupData: 'Backup local backup directory for 1Panel', + compress: 'Create snapshot file', upload: 'Upload snapshot file', thirdPartySupport: 'Only third-party accounts are supported', recoverDetail: 'Recover detail', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 296ecf23c..062f5197b 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -680,6 +680,7 @@ const message = { cleanHelper: '該操作將所有任務執行記錄、備份文件和日誌文件,是否繼續?', directory: '備份目錄', sourceDir: '備份目錄', + snapshot: '系統快照', allOptionHelper: '當前計劃任務為備份所有【{0}】,暫不支持直接下載,可在【{0}】備份列表中查看', exclusionRules: '排除規則', saveLocal: '同時保留本地備份(和雲存儲保留份數一致)', @@ -1001,15 +1002,13 @@ const message = { snapshot: '快照', status: '快照狀態', - panelBin: '備份 1Panel 二進製', - panelCtl: '備份 1Panel 腳本', - panelService: '備份 1Panel 服務', - panelInfo: '備份 1Panel 基礎信息', - daemonJson: '備份 Docker 配置', - appData: '備份 1Panel 應用', + panelInfo: '寫入 1Panel 基礎信息', + panelBin: '備份 1Panel 系統文件', + daemonJson: '備份 Docker 配置文件', + appData: '備份 1Panel 已安裝應用', panelData: '備份 1Panel 數據目錄', backupData: '備份 1Panel 本地備份目錄', - compress: '壓縮快照文件', + compress: '製作快照文件', upload: '上傳快照文件', thirdPartySupport: '僅支持第三方賬號', recoverDetail: '恢復詳情', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 25209e9b5..4c53c58db 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -680,6 +680,7 @@ const message = { cleanHelper: '该操作将所有任务执行记录、备份文件和日志文件,是否继续?', directory: '备份目录', sourceDir: '备份目录', + snapshot: '系统快照', allOptionHelper: '当前计划任务为备份所有【{0}】,暂不支持直接下载,可在【{0}】备份列表中查看', exclusionRules: '排除规则', saveLocal: '同时保留本地备份(和云存储保留份数一致)', @@ -1001,15 +1002,13 @@ const message = { snapshot: '快照', status: '快照状态', - panelBin: '备份 1Panel 二进制', - panelCtl: '备份 1Panel 脚本', - panelService: '备份 1Panel 服务', - panelInfo: '备份 1Panel 基础信息', - daemonJson: '备份 Docker 配置', - appData: '备份 1Panel 应用', + panelInfo: '写入 1Panel 基础信息', + panelBin: '备份 1Panel 系统文件', + daemonJson: '备份 Docker 配置文件', + appData: '备份 1Panel 已安装应用', panelData: '备份 1Panel 数据目录', backupData: '备份 1Panel 本地备份目录', - compress: '压缩快照文件', + compress: '制作快照文件', upload: '上传快照文件', thirdPartySupport: '仅支持第三方账号', recoverDetail: '恢复详情', diff --git a/frontend/src/views/cronjob/operate/index.vue b/frontend/src/views/cronjob/operate/index.vue index 48a7f546f..1f903f20f 100644 --- a/frontend/src/views/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/operate/index.vue @@ -23,6 +23,7 @@ + @@ -165,12 +166,13 @@
- +
+ +
{{ $t('cronjob.targetHelper') }} @@ -184,7 +186,9 @@
- + {{ $t('cronjob.saveLocal') }} @@ -448,6 +452,20 @@ const changeType = () => { dialogData.value.rowData.hour = 1; dialogData.value.rowData.minute = 30; 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': dialogData.value.rowData.specType = 'perDay'; dialogData.value.rowData.hour = 1; @@ -504,7 +522,8 @@ function isBackup() { dialogData.value.rowData!.type === 'app' || dialogData.value.rowData!.type === 'website' || dialogData.value.rowData!.type === 'database' || - dialogData.value.rowData!.type === 'directory' + dialogData.value.rowData!.type === 'directory' || + dialogData.value.rowData!.type === 'snapshot' ); } diff --git a/frontend/src/views/cronjob/record/index.vue b/frontend/src/views/cronjob/record/index.vue index acc063e92..f993f9ad6 100644 --- a/frontend/src/views/cronjob/record/index.vue +++ b/frontend/src/views/cronjob/record/index.vue @@ -167,7 +167,7 @@ {{ dialogData.rowData!.targetDir }} {{ dialogData.rowData!.retainCopies }} +