package service import ( "context" "encoding/json" "fmt" "os" "path" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/glebarez/sqlite" "github.com/pkg/errors" "gorm.io/gorm" ) func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion")) req.Name = fmt.Sprintf("1panel-%s-linux-%s-%s", versionItem.Value, loadOs(), time.Now().Format(constant.DateTimeSlimLayout)) appItem, _ := json.Marshal(req.AppData) panelItem, _ := json.Marshal(req.PanelData) backupItem, _ := json.Marshal(req.BackupData) snap := model.Snapshot{ Name: req.Name, TaskID: req.TaskID, Secret: req.Secret, Description: req.Description, SourceAccountIDs: req.SourceAccountIDs, DownloadAccountID: req.DownloadAccountID, AppData: string(appItem), PanelData: string(panelItem), BackupData: string(backupItem), WithMonitorData: req.WithMonitorData, WithLoginLog: req.WithLoginLog, WithOperationLog: req.WithOperationLog, WithTaskLog: req.WithTaskLog, WithSystemLog: req.WithSystemLog, Version: versionItem.Value, Status: constant.StatusWaiting, } if err := snapshotRepo.Create(&snap); err != nil { global.LOG.Errorf("create snapshot record to db failed, err: %v", err) return err } req.ID = snap.ID if err := u.HandleSnapshot(req); err != nil { return err } return nil } func (u *SnapshotService) SnapshotReCreate(id uint) error { snap, err := snapshotRepo.Get(commonRepo.WithByID(id)) if err != nil { return err } taskModel, err := taskRepo.GetFirst(taskRepo.WithResourceID(snap.ID), commonRepo.WithByType(task.TaskScopeSnapshot)) if err != nil { return err } var req dto.SnapshotCreate _ = copier.Copy(&req, snap) if err := json.Unmarshal([]byte(snap.PanelData), &req.PanelData); err != nil { return err } if err := json.Unmarshal([]byte(snap.AppData), &req.AppData); err != nil { return err } if err := json.Unmarshal([]byte(snap.BackupData), &req.BackupData); err != nil { return err } req.TaskID = taskModel.ID if err := u.HandleSnapshot(req); err != nil { return err } return nil } func (u *SnapshotService) HandleSnapshot(req dto.SnapshotCreate) error { taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeSnapshot, req.TaskID, req.ID) if err != nil { global.LOG.Errorf("new task for create snapshot failed, err: %v", err) return err } rootDir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", req.Name) itemHelper := snapHelper{SnapID: req.ID, Task: *taskItem, FileOp: files.NewFileOp(), Ctx: context.Background()} baseDir := path.Join(rootDir, "base") _ = os.MkdirAll(baseDir, os.ModePerm) go func() { taskItem.AddSubTaskWithAlias( "SnapDBInfo", func(t *task.Task) error { return loadDbConn(&itemHelper, rootDir, req) }, nil, ) if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapBaseInfo" { taskItem.AddSubTaskWithAlias( "SnapBaseInfo", func(t *task.Task) error { return snapBaseData(itemHelper, baseDir) }, nil, ) req.InterruptStep = "" } if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapInstallApp" { taskItem.AddSubTaskWithAlias( "SnapInstallApp", func(t *task.Task) error { return snapAppImage(itemHelper, req, rootDir) }, nil, ) req.InterruptStep = "" } if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapLocalBackup" { taskItem.AddSubTaskWithAlias( "SnapLocalBackup", func(t *task.Task) error { return snapBackupData(itemHelper, req, rootDir) }, nil, ) req.InterruptStep = "" } if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapPanelData" { taskItem.AddSubTaskWithAlias( "SnapPanelData", func(t *task.Task) error { return snapPanelData(itemHelper, req, rootDir) }, nil, ) req.InterruptStep = "" } taskItem.AddSubTask( "SnapCloseDBConn", func(t *task.Task) error { taskItem.Log("######################## 6 / 8 ########################") closeDatabase(itemHelper.snapAgentDB) closeDatabase(itemHelper.snapCoreDB) return nil }, nil, ) if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapCompress" { taskItem.AddSubTaskWithAlias( "SnapCompress", func(t *task.Task) error { return snapCompress(itemHelper, rootDir, req.Secret) }, nil, ) req.InterruptStep = "" } if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" { taskItem.AddSubTaskWithAlias( "SnapUpload", func(t *task.Task) error { return snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir)) }, nil, ) req.InterruptStep = "" } if err := taskItem.Execute(); err != nil { _ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep}) return } _ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""}) _ = os.RemoveAll(rootDir) }() return nil } type snapHelper struct { SnapID uint snapAgentDB *gorm.DB snapCoreDB *gorm.DB Ctx context.Context FileOp files.FileOp Wg *sync.WaitGroup Task task.Task } func loadDbConn(snap *snapHelper, targetDir string, req dto.SnapshotCreate) error { snap.Task.Log("######################## 1 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapDBInfo")) pathDB := path.Join(global.CONF.System.BaseDir, "1panel/db") err := snap.FileOp.CopyDir(pathDB, targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", pathDB), err) if err != nil { return err } agentDb, err := newSnapDB(path.Join(targetDir, "db"), "agent.db") snap.Task.LogWithStatus(i18n.GetWithName("SnapNewDB", "agent"), err) if err != nil { return err } snap.snapAgentDB = agentDb coreDb, err := newSnapDB(path.Join(targetDir, "db"), "core.db") snap.Task.LogWithStatus(i18n.GetWithName("SnapNewDB", "core"), err) if err != nil { return err } snap.snapCoreDB = coreDb if !req.WithMonitorData { err = os.Remove(path.Join(targetDir, "db/monitor.db")) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteMonitor"), err) if err != nil { return err } } if !req.WithOperationLog { err = snap.snapCoreDB.Exec("DELETE FROM operation_logs").Error snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteOperationLog"), err) if err != nil { return err } } if !req.WithLoginLog { err = snap.snapCoreDB.Exec("DELETE FROM login_logs").Error snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteLoginLog"), err) if err != nil { return err } } _ = snap.snapAgentDB.Model(&model.Setting{}).Where("key = ?", "SystemIP").Updates(map[string]interface{}{"value": ""}).Error _ = snap.snapAgentDB.Where("id = ?", snap.SnapID).Delete(&model.Snapshot{}).Error return nil } func snapBaseData(snap snapHelper, targetDir string) error { snap.Task.Log("######################## 2 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapBaseInfo")) err := common.CopyFile("/usr/local/bin/1panel", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel"), err) if err != nil { return err } err = common.CopyFile("/usr/local/bin/1panel_agent", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel_agent"), err) if err != nil { return err } err = common.CopyFile("/usr/local/bin/1pctl", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1pctl"), err) if err != nil { return err } err = common.CopyFile("/etc/systemd/system/1panel.service", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel.service"), err) if err != nil { return err } err = common.CopyFile("/etc/systemd/system/1panel_agent.service", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel_agent.service"), err) if err != nil { return err } if snap.FileOp.Stat("/etc/docker/daemon.json") { err = common.CopyFile("/etc/docker/daemon.json", targetDir) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/docker/daemon.json"), err) if err != nil { return err } } remarkInfo, _ := json.MarshalIndent(SnapshotJson{ BaseDir: global.CONF.System.BaseDir, BackupDataDir: global.CONF.System.Backup, }, "", "\t") err = os.WriteFile(path.Join(targetDir, "snapshot.json"), remarkInfo, 0640) snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", path.Join(targetDir, "snapshot.json")), err) if err != nil { return err } return nil } func snapAppImage(snap snapHelper, req dto.SnapshotCreate, targetDir string) error { snap.Task.Log("######################## 3 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapInstallApp")) var imageList []string existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG") existImages := strings.Split(existStr, "\n") for _, app := range req.AppData { for _, item := range app.Children { if item.Label == "appImage" && item.IsCheck { for _, existImage := range existImages { if existImage == item.Name { imageList = append(imageList, item.Name) } } } } } if len(imageList) != 0 { snap.Task.Logf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz")) std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz")) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDockerSave"), errors.New(std)) if err != nil { snap.Task.LogFailedWithErr(i18n.GetMsgByKey("SnapDockerSave"), errors.New(std)) return errors.New(std) } snap.Task.LogSuccess(i18n.GetMsgByKey("SnapDockerSave")) } return nil } func snapBackupData(snap snapHelper, req dto.SnapshotCreate, targetDir string) error { snap.Task.Log("######################## 4 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapLocalBackup")) excludes := loadBackupExcludes(snap, req.BackupData) for _, item := range req.AppData { for _, itemApp := range item.Children { if itemApp.Label == "appBackup" { excludes = append(excludes, loadAppBackupExcludes([]dto.DataTree{itemApp})...) } } } err := snap.FileOp.TarGzCompressPro(false, global.CONF.System.Backup, path.Join(targetDir, "1panel_backup.tar.gz"), "", strings.Join(excludes, ";")) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressBackup"), err) return err } func loadBackupExcludes(snap snapHelper, req []dto.DataTree) []string { var excludes []string for _, item := range req { if len(item.Children) == 0 { if item.IsCheck { continue } if strings.HasPrefix(item.Path, path.Join(global.CONF.System.Backup, "system_snapshot")) { fmt.Println(strings.TrimSuffix(item.Name, ".tar.gz")) if err := snap.snapAgentDB.Debug().Where("name = ? AND download_account_id = ?", strings.TrimSuffix(item.Name, ".tar.gz"), "1").Delete(&model.Snapshot{}).Error; err != nil { snap.Task.LogWithStatus("delete snapshot from database", err) } } else { fmt.Println(strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path)) if err := snap.snapAgentDB.Debug().Where("file_dir = ? AND file_name = ?", strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path)).Delete(&model.BackupRecord{}).Error; err != nil { snap.Task.LogWithStatus("delete backup file from database", err) } } excludes = append(excludes, "."+strings.TrimPrefix(item.Path, global.CONF.System.Backup)) } else { excludes = append(excludes, loadBackupExcludes(snap, item.Children)...) } } return excludes } func loadAppBackupExcludes(req []dto.DataTree) []string { var excludes []string for _, item := range req { if len(item.Children) == 0 { if !item.IsCheck { excludes = append(excludes, "."+strings.TrimPrefix(item.Path, path.Join(global.CONF.System.Backup))) } } else { excludes = append(excludes, loadAppBackupExcludes(item.Children)...) } } return excludes } func snapPanelData(snap snapHelper, req dto.SnapshotCreate, targetDir string) error { snap.Task.Log("######################## 5 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapPanelData")) excludes := loadPanelExcludes(req.PanelData) for _, item := range req.AppData { for _, itemApp := range item.Children { if itemApp.Label == "appData" { excludes = append(excludes, loadPanelExcludes([]dto.DataTree{itemApp})...) } } } excludes = append(excludes, "./tmp") excludes = append(excludes, "./cache") excludes = append(excludes, "./uploads") excludes = append(excludes, "./db") excludes = append(excludes, "./resource") if !req.WithSystemLog { excludes = append(excludes, "./log/1Panel*") } if !req.WithTaskLog { excludes = append(excludes, "./log/App") excludes = append(excludes, "./log/Snapshot") excludes = append(excludes, "./log/AppStore") excludes = append(excludes, "./log/Website") } rootDir := path.Join(global.CONF.System.BaseDir, "1panel") if strings.Contains(global.CONF.System.Backup, rootDir) { excludes = append(excludes, "."+strings.ReplaceAll(global.CONF.System.Backup, rootDir, "")) } ignoreVal, _ := settingRepo.Get(settingRepo.WithByKey("SnapshotIgnore")) rules := strings.Split(ignoreVal.Value, ",") for _, ignore := range rules { if len(ignore) == 0 || cmd.CheckIllegal(ignore) { continue } excludes = append(excludes, "."+strings.ReplaceAll(ignore, rootDir, "")) } err := snap.FileOp.TarGzCompressPro(false, rootDir, path.Join(targetDir, "1panel_data.tar.gz"), "", strings.Join(excludes, ";")) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressPanel"), err) return err } func loadPanelExcludes(req []dto.DataTree) []string { var excludes []string for _, item := range req { if len(item.Children) == 0 { if !item.IsCheck { excludes = append(excludes, "."+strings.TrimPrefix(item.Path, path.Join(global.CONF.System.BaseDir, "1panel"))) } } else { excludes = append(excludes, loadPanelExcludes(item.Children)...) } } return excludes } func snapCompress(snap snapHelper, rootDir string, secret string) error { snap.Task.Log("######################## 7 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapCompress")) tmpDir := path.Join(global.CONF.System.TmpDir, "system") fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir)) err := snap.FileOp.TarGzCompressPro(true, rootDir, path.Join(tmpDir, fileName), secret, "") snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressFile"), err) if err != nil { return err } stat, err := os.Stat(path.Join(tmpDir, fileName)) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCheckCompress"), err) if err != nil { return err } size := common.LoadSizeUnit2F(float64(stat.Size())) snap.Task.Logf(i18n.GetWithName("SnapCompressSize", size)) _ = os.RemoveAll(rootDir) return nil } func snapUpload(snap snapHelper, accounts string, file string) error { snap.Task.Log("######################## 8 / 8 ########################") snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload")) source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file)) accountMap, err := NewBackupClientMap(strings.Split(accounts, ",")) snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapLoadBackup"), err) if err != nil { return err } targetAccounts := strings.Split(accounts, ",") for _, item := range targetAccounts { snap.Task.LogStart(i18n.GetWithName("SnapUploadTo", fmt.Sprintf("[%s] %s", accountMap[item].name, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))))) _, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))) snap.Task.LogWithStatus(i18n.GetWithName("SnapUploadRes", accountMap[item].name), err) if err != nil { return err } } _ = os.Remove(source) return nil } func newSnapDB(dir, file string) (*gorm.DB, error) { db, _ := gorm.Open(sqlite.Open(path.Join(dir, file)), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) sqlDB, err := db.DB() if err != nil { return nil, err } sqlDB.SetConnMaxIdleTime(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) return db, nil } func closeDatabase(db *gorm.DB) { sqlDB, err := db.DB() if err != nil { return } _ = sqlDB.Close() }