feat: 优化快照功能

This commit is contained in:
ssonglius11 2024-09-13 16:09:19 +08:00
parent 53cfb2e755
commit cac60477eb
21 changed files with 1757 additions and 813 deletions

View file

@ -7,6 +7,21 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags System Setting
// @Summary Load system snapshot data
// @Description 获取系统快照数据
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/load [get]
func (b *BaseApi) LoadSnapshotData(c *gin.Context) {
data, err := snapshotService.LoadSnapshotData()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags System Setting
// @Summary Create system snapshot
// @Description 创建系统快照

View file

@ -1,7 +1,5 @@
package dto
import "time"
type SettingInfo struct {
SystemIP string `json:"systemIP"`
DockerSockPath string `json:"dockerSockPath"`
@ -34,64 +32,6 @@ type SettingUpdate struct {
Value string `json:"value"`
}
type SnapshotStatus struct {
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"`
Size string `json:"size"`
Upload string `json:"upload"`
}
type SnapshotCreate struct {
ID uint `json:"id"`
SourceAccountIDs string `json:"sourceAccountIDs" validate:"required"`
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
Description string `json:"description" validate:"max=256"`
Secret string `json:"secret"`
}
type SnapshotRecover struct {
IsNew bool `json:"isNew"`
ReDownload bool `json:"reDownload"`
ID uint `json:"id" validate:"required"`
Secret string `json:"secret"`
}
type SnapshotBatchDelete struct {
DeleteWithFile bool `json:"deleteWithFile"`
Ids []uint `json:"ids" validate:"required"`
}
type SnapshotImport struct {
BackupAccountID uint `json:"backupAccountID"`
Names []string `json:"names"`
Description string `json:"description" validate:"max=256"`
}
type SnapshotInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description" validate:"max=256"`
From string `json:"from"`
DefaultDownload string `json:"defaultDownload"`
Status string `json:"status"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
Version string `json:"version"`
Size int64 `json:"size"`
InterruptStep string `json:"interruptStep"`
RecoverStatus string `json:"recoverStatus"`
RecoverMessage string `json:"recoverMessage"`
LastRecoveredAt string `json:"lastRecoveredAt"`
RollbackStatus string `json:"rollbackStatus"`
RollbackMessage string `json:"rollbackMessage"`
LastRollbackedAt string `json:"lastRollbackedAt"`
}
type SyncTime struct {
NtpSite string `json:"ntpSite" validate:"required"`
}

90
agent/app/dto/snapshot.go Normal file
View file

@ -0,0 +1,90 @@
package dto
import "time"
type SnapshotStatus struct {
BaseData string `json:"baseData"`
AppImage string `json:"appImage"`
PanelData string `json:"panelData"`
BackupData string `json:"backupData"`
Compress string `json:"compress"`
Size string `json:"size"`
Upload string `json:"upload"`
}
type SnapshotCreate struct {
ID uint `json:"id"`
SourceAccountIDs string `json:"sourceAccountIDs" validate:"required"`
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
Description string `json:"description" validate:"max=256"`
Secret string `json:"secret"`
AppData []DataTree `json:"appData"`
BackupData []DataTree `json:"backupData"`
PanelData []DataTree `json:"panelData"`
WithMonitorData bool `json:"withMonitorData"`
WithLoginLog bool `json:"withLoginLog"`
WithOperationLog bool `json:"withOperationLog"`
}
type SnapshotData struct {
AppData []DataTree `json:"appData"`
BackupData []DataTree `json:"backupData"`
PanelData []DataTree `json:"panelData"`
WithMonitorData bool `json:"withMonitorData"`
WithLoginLog bool `json:"withLoginLog"`
WithOperationLog bool `json:"withOperationLog"`
}
type DataTree struct {
ID string `json:"id"`
Label string `json:"label"`
Key string `json:"key"`
Name string `json:"name"`
Size uint64 `json:"size"`
IsCheck bool `json:"isCheck"`
IsDisable bool `json:"isDisable"`
Path string `json:"path"`
Children []DataTree `json:"children"`
}
type SnapshotRecover struct {
IsNew bool `json:"isNew"`
ReDownload bool `json:"reDownload"`
ID uint `json:"id" validate:"required"`
Secret string `json:"secret"`
}
type SnapshotBatchDelete struct {
DeleteWithFile bool `json:"deleteWithFile"`
Ids []uint `json:"ids" validate:"required"`
}
type SnapshotImport struct {
BackupAccountID uint `json:"backupAccountID"`
Names []string `json:"names"`
Description string `json:"description" validate:"max=256"`
}
type SnapshotInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description" validate:"max=256"`
From string `json:"from"`
DefaultDownload string `json:"defaultDownload"`
Status string `json:"status"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
Version string `json:"version"`
Size int64 `json:"size"`
InterruptStep string `json:"interruptStep"`
RecoverStatus string `json:"recoverStatus"`
RecoverMessage string `json:"recoverMessage"`
LastRecoveredAt string `json:"lastRecoveredAt"`
RollbackStatus string `json:"rollbackStatus"`
RollbackMessage string `json:"rollbackMessage"`
LastRollbackedAt string `json:"lastRollbackedAt"`
}

View file

@ -10,6 +10,13 @@ type Snapshot struct {
Message string `json:"message"`
Version string `json:"version"`
AppData string `json:"appData"`
PanelData string `json:"panelData"`
BackupData string `json:"backupData"`
WithMonitorData bool `json:"withMonitorData"`
WithLoginLog bool `json:"withLoginLog"`
WithOperationLog bool `json:"withOperationLog"`
InterruptStep string `json:"interruptStep"`
RecoverStatus string `json:"recoverStatus"`
RecoverMessage string `json:"recoverMessage"`
@ -21,11 +28,10 @@ type Snapshot struct {
type SnapshotStatus struct {
BaseModel
SnapID uint `json:"snapID"`
Panel string `json:"panel" gorm:"default:Running"`
PanelInfo string `json:"panelInfo" gorm:"default:Running"`
DaemonJson string `json:"daemonJson" gorm:"default:Running"`
AppData string `json:"appData" gorm:"default:Running"`
SnapID uint `json:"snapID"`
BaseData string `json:"baseData" gorm:"default:Running"`
AppImage string `json:"appImage" gorm:"default:Running"`
PanelData string `json:"panelData" gorm:"default:Running"`
BackupData string `json:"backupData" gorm:"default:Running"`

View file

@ -16,7 +16,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/image"
"github.com/google/uuid"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/shirou/gopsutil/v3/host"
@ -28,6 +32,7 @@ type SnapshotService struct {
type ISnapshotService interface {
SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error)
LoadSnapshotData() (dto.SnapshotData, error)
SnapshotCreate(req dto.SnapshotCreate) error
SnapshotRecover(req dto.SnapshotRecover) error
SnapshotRollback(req dto.SnapshotRecover) error
@ -92,6 +97,37 @@ func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
return nil
}
func (u *SnapshotService) LoadSnapshotData() (dto.SnapshotData, error) {
var (
data dto.SnapshotData
err error
)
fileOp := fileUtils.NewFileOp()
data.AppData, err = loadApps(fileOp)
if err != nil {
return data, err
}
data.PanelData, err = loadPanelFile(fileOp)
if err != nil {
return data, err
}
itemBackups, err := loadFile(global.CONF.System.Backup, 8, fileOp)
if err != nil {
return data, err
}
for i, item := range itemBackups {
if item.Label == "app" {
data.BackupData = append(itemBackups[:i], itemBackups[i+1:]...)
break
}
}
data.WithLoginLog = true
data.WithOperationLog = true
data.WithMonitorData = false
return data, nil
}
func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error {
return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
}
@ -109,16 +145,9 @@ func (u *SnapshotService) LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, erro
}
type SnapshotJson struct {
OldBaseDir string `json:"oldBaseDir"`
OldDockerDataDir string `json:"oldDockerDataDir"`
OldBackupDataDir string `json:"oldBackupDataDir"`
OldPanelDataDir string `json:"oldPanelDataDir"`
BaseDir string `json:"baseDir"`
DockerDataDir string `json:"dockerDataDir"`
BackupDataDir string `json:"backupDataDir"`
PanelDataDir string `json:"panelDataDir"`
LiveRestoreEnabled bool `json:"liveRestoreEnabled"`
BaseDir string `json:"baseDir"`
BackupDataDir string `json:"backupDataDir"`
Size uint64 `json:"size"`
}
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
@ -148,7 +177,7 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
_ = settingRepo.Update("SystemStatus", "Recovering")
go u.HandleSnapshotRecover(snap, true, req)
go u.HandleSnapshotRecover(snap, req)
return nil
}
@ -158,9 +187,11 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
if err != nil {
return err
}
req.IsNew = false
snap.InterruptStep = "Readjson"
go u.HandleSnapshotRecover(snap, false, req)
go func() {
if err := handleRollback(snap.Name); err != nil {
global.LOG.Errorf("handle roll back snapshot failed, err: %v", err)
}
}()
return nil
}
@ -194,15 +225,26 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
if isCronjob {
name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
}
rootDir = path.Join(global.CONF.System.Backup, "system", name)
rootDir = path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", name)
appItem, _ := json.Marshal(req.AppData)
panelItem, _ := json.Marshal(req.PanelData)
backupItem, _ := json.Marshal(req.BackupData)
snap = model.Snapshot{
Name: name,
Description: req.Description,
SourceAccountIDs: req.SourceAccountIDs,
DownloadAccountID: req.DownloadAccountID,
Version: versionItem.Value,
Status: constant.StatusWaiting,
AppData: string(appItem),
PanelData: string(panelItem),
BackupData: string(backupItem),
WithMonitorData: req.WithMonitorData,
WithLoginLog: req.WithLoginLog,
WithOperationLog: req.WithOperationLog,
Version: versionItem.Value,
Status: constant.StatusWaiting,
}
_ = snapshotRepo.Create(&snap)
snapStatus.SnapID = snap.ID
@ -218,57 +260,43 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
snapStatus.SnapID = snap.ID
_ = snapshotRepo.CreateStatus(&snapStatus)
}
rootDir = path.Join(global.CONF.System.Backup, fmt.Sprintf("system/%s", snap.Name))
rootDir = path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", 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)
baseDir := path.Join(rootDir, "base")
_ = os.MkdirAll(baseDir, os.ModePerm)
if err := loadDbConn(&itemHelper, rootDir, req); err != nil {
return "", fmt.Errorf("load snapshot db conn failed, err: %v", err)
}
jsonItem := SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: global.CONF.System.Backup,
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
}
loadLogByStatus(snapStatus, logPath)
if snapStatus.PanelInfo != constant.StatusDone {
if snapStatus.BaseData != constant.StatusDone {
wg.Add(1)
go snapJson(itemHelper, jsonItem, rootDir)
go snapBaseData(itemHelper, baseDir)
}
if snapStatus.Panel != constant.StatusDone {
if snapStatus.AppImage != 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)
go snapAppImage(itemHelper, req, rootDir)
}
if snapStatus.BackupData != constant.StatusDone {
wg.Add(1)
go snapBackup(itemHelper, backupPanelDir)
go snapBackupData(itemHelper, req, rootDir)
}
if snapStatus.PanelData != constant.StatusDone {
wg.Add(1)
go snapPanelData(itemHelper, req, rootDir)
}
if !isCronjob {
go func() {
wg.Wait()
closeDatabase(itemHelper.snapAgentDB)
closeDatabase(itemHelper.snapCoreDB)
if !checkIsAllDone(snap.ID) {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
if snapStatus.PanelData != constant.StatusDone {
snapPanelData(itemHelper, backupPanelDir)
}
if snapStatus.PanelData != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
if snapStatus.Compress != constant.StatusDone {
snapCompress(itemHelper, rootDir, secret)
}
@ -284,23 +312,20 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
return
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
// _ = snapshotRepo.DeleteStatus(itemHelper.SnapID)
_ = os.RemoveAll(rootDir)
}()
return "", nil
}
wg.Wait()
closeDatabase(itemHelper.snapAgentDB)
closeDatabase(itemHelper.snapCoreDB)
if !checkIsAllDone(snap.ID) {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
loadLogByStatus(snapStatus, logPath)
return snap.Name, fmt.Errorf("snapshot %s backup failed", snap.Name)
}
loadLogByStatus(snapStatus, logPath)
snapPanelData(itemHelper, backupPanelDir)
if snapStatus.PanelData != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
loadLogByStatus(snapStatus, logPath)
return snap.Name, fmt.Errorf("snapshot %s 1panel data failed", snap.Name)
}
loadLogByStatus(snapStatus, logPath)
snapCompress(itemHelper, rootDir, secret)
if snapStatus.Compress != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
@ -316,6 +341,7 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
loadLogByStatus(snapStatus, logPath)
_ = os.RemoveAll(rootDir)
return snap.Name, nil
}
@ -341,71 +367,6 @@ func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error {
return nil
}
func updateRecoverStatus(id uint, isRecover bool, interruptStep, status, message string) {
if isRecover {
if status != constant.StatusSuccess {
global.LOG.Errorf("recover failed, err: %s", message)
}
if err := snapshotRepo.Update(id, map[string]interface{}{
"interrupt_step": interruptStep,
"recover_status": status,
"recover_message": message,
"last_recovered_at": time.Now().Format(constant.DateTimeLayout),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
_ = settingRepo.Update("SystemStatus", "Free")
return
}
_ = settingRepo.Update("SystemStatus", "Free")
if status == constant.StatusSuccess {
if err := snapshotRepo.Update(id, map[string]interface{}{
"recover_status": "",
"recover_message": "",
"interrupt_step": "",
"rollback_status": "",
"rollback_message": "",
"last_rollbacked_at": time.Now().Format(constant.DateTimeLayout),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
return
}
global.LOG.Errorf("rollback failed, err: %s", message)
if err := snapshotRepo.Update(id, map[string]interface{}{
"rollback_status": status,
"rollback_message": message,
"last_rollbacked_at": time.Now().Format(constant.DateTimeLayout),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
}
func (u *SnapshotService) handleUnTar(sourceDir, targetDir string, secret string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
}
commands := ""
if len(secret) != 0 {
extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceDir + " | "
commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1")
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar zxvfC %s %s", sourceDir, targetDir)
global.LOG.Debug(commands)
}
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
if err != nil {
if len(stdout) != 0 {
global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err)
return fmt.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err)
}
}
return nil
}
func rebuildAllAppInstall() error {
global.LOG.Debug("start to rebuild all app")
appInstalls, err := appInstallRepo.ListBy()
@ -449,17 +410,14 @@ func checkIsAllDone(snapID uint) bool {
}
func checkAllDone(status model.SnapshotStatus) (bool, string) {
if status.Panel != constant.StatusDone {
return false, status.Panel
if status.BaseData != constant.StatusDone {
return false, status.BaseData
}
if status.PanelInfo != constant.StatusDone {
return false, status.PanelInfo
if status.PanelData != constant.StatusDone {
return false, status.PanelData
}
if status.DaemonJson != constant.StatusDone {
return false, status.DaemonJson
}
if status.AppData != constant.StatusDone {
return false, status.AppData
if status.AppImage != constant.StatusDone {
return false, status.AppImage
}
if status.BackupData != constant.StatusDone {
return false, status.BackupData
@ -469,10 +427,8 @@ func checkAllDone(status model.SnapshotStatus) (bool, string) {
func loadLogByStatus(status model.SnapshotStatus, logPath 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 base files: %s \n", status.BaseData)
logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppImage)
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.Compress)
@ -544,3 +500,154 @@ func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotInfo, error) {
wg.Wait()
return datas, nil
}
func loadApps(fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
var data []dto.DataTree
apps, err := appInstallRepo.ListBy()
if err != nil {
return data, err
}
client, err := docker.NewDockerClient()
hasDockerClient := true
if err != nil {
hasDockerClient = false
global.LOG.Errorf("new docker client failed, err: %v", err)
} else {
defer client.Close()
}
imageList, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
hasDockerClient = false
global.LOG.Errorf("load image list failed, err: %v", err)
}
for _, app := range apps {
itemApp := dto.DataTree{ID: uuid.NewString(), Label: fmt.Sprintf("%s - %s", app.App.Name, app.Name), Key: app.App.Key, Name: app.Name}
appPath := path.Join(global.CONF.System.BaseDir, "1panel/apps", app.App.Key, app.Name)
itemAppData := dto.DataTree{ID: uuid.NewString(), Label: "appData", Key: app.App.Key, Name: app.Name, IsCheck: true, Path: appPath}
sizeItem, err := fileOp.GetDirSize(appPath)
if err == nil {
itemAppData.Size = uint64(sizeItem)
}
itemApp.Size += itemAppData.Size
itemApp.Children = append(itemApp.Children, itemAppData)
appBackupPath := path.Join(global.CONF.System.BaseDir, "1panel/backup/app", app.App.Key, app.Name)
itemAppBackupTree, err := loadFile(appBackupPath, 8, fileOp)
itemAppBackup := dto.DataTree{ID: uuid.NewString(), Label: "appBackup", IsCheck: true, Children: itemAppBackupTree, Path: appBackupPath}
if err == nil {
backupSizeItem, err := fileOp.GetDirSize(appBackupPath)
if err == nil {
itemAppBackup.Size = uint64(backupSizeItem)
itemApp.Size += itemAppBackup.Size
}
itemApp.Children = append(itemApp.Children, itemAppBackup)
}
itemAppImage := dto.DataTree{ID: uuid.NewString(), Label: "appImage"}
stdout, err := cmd.Execf("cat %s | grep image: ", path.Join(global.CONF.System.BaseDir, "1panel/apps", app.App.Key, app.Name, "docker-compose.yml"))
if err != nil {
itemApp.Children = append(itemApp.Children, itemAppImage)
data = append(data, itemApp)
continue
}
itemAppImage.Name = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(stdout), "\n", ""), "image: ", "")
if !hasDockerClient {
itemApp.Children = append(itemApp.Children, itemAppImage)
data = append(data, itemApp)
continue
}
for _, imageItem := range imageList {
for _, tag := range imageItem.RepoTags {
if tag == itemAppImage.Name {
itemAppImage.Size = uint64(imageItem.Size)
break
}
}
}
itemApp.Children = append(itemApp.Children, itemAppImage)
data = append(data, itemApp)
}
return data, nil
}
func loadPanelFile(fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
var data []dto.DataTree
snapFiles, err := os.ReadDir(path.Join(global.CONF.System.BaseDir, "1panel"))
if err != nil {
return data, err
}
for _, fileItem := range snapFiles {
itemData := dto.DataTree{
ID: uuid.NewString(),
Label: fileItem.Name(),
IsCheck: true,
Path: path.Join(global.CONF.System.BaseDir, "1panel", fileItem.Name()),
}
switch itemData.Label {
case "agent", "conf", "db", "runtime", "secret":
itemData.IsDisable = true
case "log", "docker", "task", "clamav":
panelPath := path.Join(global.CONF.System.BaseDir, "1panel", itemData.Label)
itemData.Children, _ = loadFile(panelPath, 5, fileOp)
default:
continue
}
if fileItem.IsDir() {
sizeItem, err := fileOp.GetDirSize(path.Join(global.CONF.System.BaseDir, "1panel", itemData.Label))
if err != nil {
continue
}
itemData.Size = uint64(sizeItem)
} else {
fileInfo, err := fileItem.Info()
if err != nil {
continue
}
itemData.Size = uint64(fileInfo.Size())
}
if itemData.IsCheck && itemData.Size == 0 {
itemData.IsCheck = false
itemData.IsDisable = true
}
data = append(data, itemData)
}
return data, nil
}
func loadFile(pathItem string, index int, fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
var data []dto.DataTree
snapFiles, err := os.ReadDir(pathItem)
if err != nil {
return data, err
}
i := 0
for _, fileItem := range snapFiles {
itemData := dto.DataTree{
ID: uuid.NewString(),
Label: fileItem.Name(),
Name: fileItem.Name(),
Path: path.Join(pathItem, fileItem.Name()),
IsCheck: true,
}
if fileItem.IsDir() {
sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, itemData.Label))
if err != nil {
continue
}
itemData.Size = uint64(sizeItem)
itemData.Children, _ = loadFile(path.Join(pathItem, itemData.Label), index-1, fileOp)
} else {
fileInfo, err := fileItem.Info()
if err != nil {
continue
}
itemData.Size = uint64(fileInfo.Size())
}
data = append(data, itemData)
i++
}
return data, nil
}

View file

@ -6,145 +6,219 @@ import (
"fmt"
"os"
"path"
"regexp"
"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/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type snapHelper struct {
SnapID uint
Status *model.SnapshotStatus
Ctx context.Context
FileOp files.FileOp
Wg *sync.WaitGroup
SnapID uint
snapAgentDB *gorm.DB
snapCoreDB *gorm.DB
Status *model.SnapshotStatus
Ctx context.Context
FileOp files.FileOp
Wg *sync.WaitGroup
}
func snapJson(snap snapHelper, snapJson SnapshotJson, targetDir string) {
defer snap.Wg.Done()
_ = 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()
func loadDbConn(snap *snapHelper, targetDir string, req dto.SnapshotCreate) error {
global.LOG.Debug("start load snapshot db conn")
if err := snap.FileOp.CopyDir(path.Join(global.CONF.System.BaseDir, "1panel/db"), targetDir); err != nil {
return err
}
snap.Status.PanelInfo = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status})
agentDb, err := newSnapDB(path.Join(targetDir, "db"), "agent.db")
if err != nil {
return err
}
snap.snapAgentDB = agentDb
coreDb, err := newSnapDB(path.Join(targetDir, "db"), "core.db")
if err != nil {
return err
}
snap.snapCoreDB = coreDb
if !req.WithMonitorData {
_ = os.Remove(path.Join(targetDir, "db/monitor.db"))
}
if !req.WithOperationLog {
_ = snap.snapCoreDB.Exec("DELETE FROM operation_logs")
}
if !req.WithLoginLog {
_ = snap.snapCoreDB.Exec("DELETE FROM login_logs")
}
if err := snap.snapAgentDB.Where("id = ?", snap.SnapID).Delete(&model.Snapshot{}).Error; err != nil {
global.LOG.Errorf("delete current snapshot record failed, err: %v", err)
}
if err := snap.snapAgentDB.Where("snap_id = ?", snap.SnapID).Delete(&model.SnapshotStatus{}).Error; err != nil {
global.LOG.Errorf("delete current snapshot status record failed, err: %v", err)
}
return nil
}
func snapPanel(snap snapHelper, targetDir string) {
func snapBaseData(snap snapHelper, targetDir string) {
defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"base_data": constant.Running})
status := constant.StatusDone
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil {
if err := common.CopyFile("/usr/local/bin/1panel", targetDir); err != nil {
status = err.Error()
}
if err := common.CopyFile("/usr/local/bin/1panel_agent", targetDir); err != nil {
status = err.Error()
}
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil {
status = err.Error()
}
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil {
status = err.Error()
}
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
}
func snapDaemonJson(snap snapHelper, targetDir string) {
defer snap.Wg.Done()
status := constant.StatusDone
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(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running})
if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil {
if err := common.CopyFile("/etc/systemd/system/1panel_agent.service", targetDir); err != nil {
status = err.Error()
}
snap.Status.DaemonJson = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
if snap.FileOp.Stat("/etc/docker/daemon.json") {
if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil {
status = err.Error()
}
}
remarkInfo, _ := json.MarshalIndent(SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: global.CONF.System.Backup,
}, "", "\t")
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil {
status = err.Error()
}
snap.Status.BaseData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"base_data": status})
}
func snapAppData(snap snapHelper, targetDir string) {
func snapAppImage(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running})
appInstalls, err := appInstallRepo.ListBy()
if err != nil {
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 {
snap.Status.AppData = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
return
}
imageRegex := regexp.MustCompile(`image:\s*(.*)`)
var imageSaveList []string
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": constant.Running})
var imageList []string
existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG")
existImages := strings.Split(existStr, "\n")
duplicateMap := make(map[string]bool)
for _, app := range appInstalls {
matches := imageRegex.FindAllStringSubmatch(app.DockerCompose, -1)
for _, match := range matches {
for _, existImage := range existImages {
if match[1] == existImage && !duplicateMap[match[1]] {
imageSaveList = append(imageSaveList, match[1])
duplicateMap[match[1]] = true
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)
}
}
}
}
}
for _, runtime := range runtimes {
for _, existImage := range existImages {
if runtime.Image == existImage && !duplicateMap[runtime.Image] {
imageSaveList = append(imageSaveList, runtime.Image)
duplicateMap[runtime.Image] = true
}
}
}
if len(imageSaveList) != 0 {
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 len(imageList) != 0 {
global.LOG.Debugf("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"))
if err != nil {
snap.Status.AppData = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std})
snap.Status.AppImage = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": std})
return
}
}
snap.Status.AppData = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone})
snap.Status.AppImage = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": constant.StatusDone})
}
func snapBackup(snap snapHelper, targetDir string) {
func snapBackupData(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": constant.Running})
status := constant.StatusDone
if err := handleSnapTar(global.CONF.System.Backup, targetDir, "1panel_backup.tar.gz", "./system;./system_snapshot;", ""); err != nil {
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})...)
}
}
}
global.LOG.Debugf("excludes backup file: %v\n", strings.Join(excludes, "\n"))
if err := snap.FileOp.TarGzCompressPro(false, global.CONF.System.Backup, path.Join(targetDir, "1panel_backup.tar.gz"), "", strings.Join(excludes, ";")); err != nil {
status = err.Error()
}
snap.Status.BackupData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status})
}
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 {
global.LOG.Errorf("delete snapshot from database failed, err: %v", 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 {
global.LOG.Errorf("delete backup file from database failed, err: %v", err)
}
}
fmt.Println(strings.TrimPrefix(item.Path, global.CONF.System.Backup))
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, targetDir string) {
func snapPanelData(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
defer snap.Wg.Done()
_ = 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-*;"
if strings.Contains(global.CONF.System.Backup, dataDir) {
exclusionRules += ("." + strings.ReplaceAll(global.CONF.System.Backup, dataDir, "") + ";")
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})...)
}
}
}
global.LOG.Debugf("excludes panel file: %v\n", strings.Join(excludes, "\n"))
excludes = append(excludes, "./tmp")
excludes = append(excludes, "./cache")
excludes = append(excludes, "./uploads")
excludes = append(excludes, "./db")
excludes = append(excludes, "./resource")
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, ",")
@ -152,27 +226,37 @@ func snapPanelData(snap snapHelper, targetDir string) {
if len(ignore) == 0 || cmd.CheckIllegal(ignore) {
continue
}
exclusionRules += ("." + strings.ReplaceAll(ignore, dataDir, "") + ";")
excludes = append(excludes, "."+strings.ReplaceAll(ignore, rootDir, ""))
}
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"})
sysIP, _ := settingRepo.Get(settingRepo.WithByKey("SystemIP"))
_ = settingRepo.Update("SystemIP", "")
checkPointOfWal()
if err := handleSnapTar(dataDir, targetDir, "1panel_data.tar.gz", exclusionRules, ""); err != nil {
_ = snap.snapAgentDB.Model(&model.Setting{}).Where("key = ?", "SystemIP").Updates(map[string]interface{}{"SystemIP": ""})
if err := snap.FileOp.TarGzCompressPro(false, rootDir, path.Join(targetDir, "1panel_data.tar.gz"), "", strings.Join(excludes, ";")); err != nil {
status = err.Error()
}
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting})
snap.Status.PanelData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status})
_ = settingRepo.Update("SystemIP", sysIP.Value)
}
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) {
_ = 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 := handleSnapTar(rootDir, tmpDir, fileName, "", secret); err != nil {
if err := snap.FileOp.TarGzCompressPro(true, rootDir, path.Join(tmpDir, fileName), secret, ""); err != nil {
snap.Status.Compress = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()})
return
@ -207,12 +291,12 @@ func snapUpload(snap snapHelper, accounts string, file string) {
for _, item := range targetAccounts {
global.LOG.Debugf("start upload snapshot to %s, path: %s", item, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file)))
if _, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))); err != nil {
global.LOG.Debugf("upload to %s failed, err: %v", item, err)
global.LOG.Debugf("upload to %s failed, err: %v", accountMap[item].name, err)
snap.Status.Upload = err.Error()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
return
}
global.LOG.Debugf("upload to %s successful", item)
global.LOG.Debugf("upload to %s successful", accountMap[item].name)
}
snap.Status.Upload = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone})
@ -221,60 +305,25 @@ func snapUpload(snap snapHelper, accounts string, file string) {
_ = os.Remove(source)
}
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
}
exMap := make(map[string]struct{})
exStr := ""
excludes := strings.Split(exclusionRules, ";")
excludes = append(excludes, "*.sock")
for _, exclude := range excludes {
if len(exclude) == 0 {
continue
}
if _, ok := exMap[exclude]; ok {
continue
}
exStr += " --exclude "
exStr += exclude
exMap[exclude] = struct{}{}
}
path := ""
if strings.Contains(sourceDir, "/") {
itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "")
aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")]
if len(aheadDir) == 0 {
aheadDir = "/"
}
path += fmt.Sprintf("-C %s %s", aheadDir, itemDir)
} else {
path = sourceDir
}
commands := ""
if len(secret) != 0 {
extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out"
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+exStr, path, extraCmd, targetDir+"/"+name)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
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)
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 {
if len(stdout) != 0 {
global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err)
return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err)
}
return nil, err
}
return nil
sqlDB.SetConnMaxIdleTime(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
// global.LOG.Debug("load snapshot db conn successful!")
return db, nil
}
func checkPointOfWal() {
if err := global.DB.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
global.LOG.Errorf("handle check point failed, err: %v", err)
func closeDatabase(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}

View file

@ -1,124 +1,98 @@
package service
import (
"context"
"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/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/pkg/errors"
)
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover bool, req dto.SnapshotRecover) {
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, req dto.SnapshotRecover) {
_ = global.Cron.Stop()
defer func() {
global.Cron.Start()
}()
snapFileDir := ""
if isRecover {
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(baseDir, os.ModePerm)
}
if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload {
if err := handleDownloadSnapshot(snap, baseDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, err.Error())
return
}
global.LOG.Debugf("download snapshot file to %s successful!", baseDir)
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "Decompress" {
if err := handleUnTar(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, req.Secret); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
global.LOG.Debug("decompress snapshot file successful!", baseDir)
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "Backup" {
if err := backupBeforeRecover(snap); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err))
return
}
global.LOG.Debug("handle backup before recover successful!")
req.IsNew = true
}
snapFileDir = fmt.Sprintf("%s/%s", baseDir, snap.Name)
if _, err := os.Stat(snapFileDir); err != nil {
snapFileDir = baseDir
}
} else {
snapFileDir = fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
if _, err := os.Stat(snapFileDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "", constant.StatusFailed, fmt.Sprintf("cannot find the backup file %s, please try to recover again.", snapFileDir))
fileOp := files.NewFileOp()
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(baseDir, os.ModePerm)
}
if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload {
if err := handleDownloadSnapshot(snap, baseDir); err != nil {
updateRecoverStatus(snap.ID, "Download", constant.StatusFailed, err.Error())
return
}
global.LOG.Debugf("download snapshot file to %s successful!", baseDir)
req.IsNew = true
}
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", snapFileDir))
if req.IsNew || snap.InterruptStep == "Decompress" {
if err := fileOp.TarGzExtractPro(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, req.Secret); err != nil {
updateRecoverStatus(snap.ID, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
global.LOG.Debug("decompress snapshot file successful!", baseDir)
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "Backup" {
if err := backupBeforeRecover(snap.Name); err != nil {
updateRecoverStatus(snap.ID, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err))
return
}
global.LOG.Debug("handle backup before recover successful!")
req.IsNew = true
}
snapFileDir := fmt.Sprintf("%s/%s", baseDir, snap.Name)
if _, err := os.Stat(snapFileDir); err != nil {
snapFileDir = baseDir
}
snapJson, err := u.readFromJson(path.Join(snapFileDir, "base/snapshot.json"))
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
updateRecoverStatus(snap.ID, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
if snap.InterruptStep == "Readjson" {
req.IsNew = true
}
if isRecover && (req.IsNew || snap.InterruptStep == "AppData") {
if req.IsNew || snap.InterruptStep == "AppImage" {
if err := recoverAppData(snapFileDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "DockerDir", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err))
updateRecoverStatus(snap.ID, "AppImage", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err))
return
}
global.LOG.Debug("recover app data from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "DaemonJson" {
fileOp := files.NewFileOp()
if err := recoverDaemonJson(snapFileDir, fileOp); err != nil {
updateRecoverStatus(snap.ID, isRecover, "DaemonJson", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover daemon.json from snapshot file successful!")
global.LOG.Debug("recover app images from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
if req.IsNew || snap.InterruptStep == "BaseData" {
if err := recoverBaseData(path.Join(snapFileDir, "base"), fileOp); err != nil {
updateRecoverStatus(snap.ID, "BaseData", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel binary from snapshot file successful!")
global.LOG.Debug("recover base data from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PctlBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
if req.IsNew || snap.InterruptStep == "DBData" {
if err := recoverDBData(path.Join(snapFileDir, "db"), fileOp); err != nil {
updateRecoverStatus(snap.ID, "DBData", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1pctl from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelService" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel service from snapshot file successful!")
global.LOG.Debug("recover db data from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelBackups" {
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_backup.tar.gz"), snapJson.BackupDataDir, ""); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBackups", constant.StatusFailed, err.Error())
if err := fileOp.TarGzExtractPro(path.Join(snapFileDir, "/1panel_backup.tar.gz"), snapJson.BackupDataDir, ""); err != nil {
updateRecoverStatus(snap.ID, "1PanelBackups", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel backups from snapshot file successful!")
@ -126,9 +100,8 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
}
if req.IsNew || snap.InterruptStep == "1PanelData" {
checkPointOfWal()
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), ""); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelData", constant.StatusFailed, err.Error())
if err := fileOp.TarGzExtractPro(path.Join(snapFileDir, "/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), ""); err != nil {
updateRecoverStatus(snap.ID, "1PanelData", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel data from snapshot file successful!")
@ -138,51 +111,11 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
restartCompose(path.Join(snapJson.BaseDir, "1panel/docker/compose"))
global.LOG.Info("recover successful")
if !isRecover {
oriPath := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
global.LOG.Debugf("remove the file %s after the operation is successful", oriPath)
_ = os.RemoveAll(oriPath)
} else {
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir))
}
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir))
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
}
func backupBeforeRecover(snap model.Snapshot) error {
baseDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
var wg sync.WaitGroup
var status model.SnapshotStatus
itemHelper := snapHelper{SnapID: 0, Status: &status, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
jsonItem := SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: global.CONF.System.Backup,
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
}
_ = os.MkdirAll(path.Join(baseDir, "1panel"), os.ModePerm)
_ = os.MkdirAll(path.Join(baseDir, "docker"), os.ModePerm)
wg.Add(4)
itemHelper.Wg = &wg
go snapJson(itemHelper, jsonItem, baseDir)
go snapPanel(itemHelper, path.Join(baseDir, "1panel"))
go snapDaemonJson(itemHelper, path.Join(baseDir, "docker"))
go snapBackup(itemHelper, path.Join(baseDir, "1panel"))
wg.Wait()
itemHelper.Status.AppData = constant.StatusDone
allDone, msg := checkAllDone(status)
if !allDone {
return errors.New(msg)
}
snapPanelData(itemHelper, path.Join(baseDir, "1panel"))
if status.PanelData != constant.StatusDone {
return errors.New(status.PanelData)
}
return nil
}
func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error {
account, client, err := NewBackupClientWithID(snap.DownloadAccountID)
if err != nil {
@ -202,18 +135,34 @@ func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error {
}
func recoverAppData(src string) error {
if _, err := os.Stat(path.Join(src, "docker/docker_image.tar")); err != nil {
if _, err := os.Stat(path.Join(src, "images.tar.gz")); err != nil {
global.LOG.Debug("no such docker images in snapshot")
return nil
}
std, err := cmd.Execf("docker load < %s", path.Join(src, "docker/docker_image.tar"))
std, err := cmd.Execf("docker load < %s", path.Join(src, "images.tar.gz"))
if err != nil {
return errors.New(std)
}
return err
}
func recoverDaemonJson(src string, fileOp files.FileOp) error {
func recoverBaseData(src string, fileOp files.FileOp) error {
if err := fileOp.CopyFile(path.Join(src, "1pctl"), "/usr/local/bin"); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(src, "1panel"), "/usr/local/bin"); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(src, "1panel_agent"), "/usr/local/bin"); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(src, "1panel.service"), "/etc/systemd/system"); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(src, "1panel_agent.service"), "/etc/systemd/system"); err != nil {
return err
}
daemonJsonPath := "/etc/docker/daemon.json"
_, errSrc := os.Stat(path.Join(src, "docker/daemon.json"))
_, errPath := os.Stat(daemonJsonPath)
@ -231,14 +180,8 @@ func recoverDaemonJson(src string, fileOp files.FileOp) error {
return nil
}
func recoverPanel(src string, dst string) error {
if _, err := os.Stat(src); err != nil {
return fmt.Errorf("file is not found in %s, err: %v", src, err)
}
if err := common.CopyFile(src, dst); err != nil {
return fmt.Errorf("cp file failed, err: %v", err)
}
return nil
func recoverDBData(src string, fileOp files.FileOp) error {
return fileOp.CopyDir(src, path.Join(global.CONF.System.BaseDir, "1panel", "db"))
}
func restartCompose(composePath string) {
@ -259,3 +202,86 @@ func restartCompose(composePath string) {
}
global.LOG.Debug("restart all compose successful!")
}
func backupBeforeRecover(name string) error {
rootDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, name)
baseDir := path.Join(rootDir, "base")
if _, err := os.Stat(baseDir); err != nil {
_ = os.MkdirAll(baseDir, os.ModePerm)
}
FileOp := files.NewFileOp()
if err := FileOp.CopyDirWithExclude(path.Join(global.CONF.System.BaseDir, "1panel"), rootDir, []string{"cache", "tmp"}); err != nil {
return err
}
if err := FileOp.CopyDir(global.CONF.System.Backup, rootDir); err != nil {
return err
}
if err := FileOp.CopyFile("/usr/local/bin/1pctl", baseDir); err != nil {
return err
}
if err := FileOp.CopyFile("/usr/local/bin/1panel", baseDir); err != nil {
return err
}
if err := FileOp.CopyFile("/usr/local/bin/1panel_agent", baseDir); err != nil {
return err
}
if err := FileOp.CopyFile("/etc/systemd/system/1panel.service", baseDir); err != nil {
return err
}
if err := FileOp.CopyFile("/etc/systemd/system/1panel_agent.service", baseDir); err != nil {
return err
}
if err := FileOp.CopyFile("/etc/docker/daemon.json", baseDir); err != nil {
return err
}
return nil
}
func handleRollback(name string) error {
rootDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, name)
baseDir := path.Join(rootDir, "base")
FileOp := files.NewFileOp()
if err := FileOp.CopyDir(path.Join(rootDir, "1panel"), global.CONF.System.BaseDir); err != nil {
return err
}
if err := FileOp.CopyDir(path.Join(rootDir, "backup"), path.Dir(global.CONF.System.Backup)); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "1pctl"), "/usr/local/bin/1pctl"); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "1panel"), "/usr/local/bin/1panel"); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "1panel_agent"), "/usr/local/bin/1panel_agent"); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "1panel.service"), "/etc/systemd/system/1panel.service"); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "1panel_agent.service"), "/etc/systemd/system/1panel_agent.service"); err != nil {
return err
}
if err := FileOp.CopyFile(path.Join(baseDir, "daemon.json"), "/etc/docker/daemon.json"); err != nil {
return err
}
_ = os.RemoveAll(rootDir)
return nil
}
func updateRecoverStatus(id uint, interruptStep, status, message string) {
if status != constant.StatusSuccess {
global.LOG.Errorf("recover failed, err: %s", message)
}
if err := snapshotRepo.Update(id, map[string]interface{}{
"interrupt_step": interruptStep,
"recover_status": status,
"recover_message": message,
"last_recovered_at": time.Now().Format(constant.DateTimeLayout),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
_ = settingRepo.Update("SystemStatus", "Free")
}

View file

@ -72,18 +72,18 @@ func handleSnapStatus() {
status, _ := snapRepo.GetStatusList()
for _, item := range status {
updates := make(map[string]interface{})
if item.Panel == constant.StatusRunning {
updates["panel"] = constant.StatusFailed
}
if item.PanelInfo == constant.StatusRunning {
updates["panel_info"] = constant.StatusFailed
}
if item.DaemonJson == constant.StatusRunning {
updates["daemon_json"] = constant.StatusFailed
}
if item.AppData == constant.StatusRunning {
updates["app_data"] = constant.StatusFailed
}
// if item.Panel == constant.StatusRunning {
// updates["panel"] = constant.StatusFailed
// }
// if item.PanelInfo == constant.StatusRunning {
// updates["panel_info"] = constant.StatusFailed
// }
// if item.DaemonJson == constant.StatusRunning {
// updates["daemon_json"] = constant.StatusFailed
// }
// if item.AppData == constant.StatusRunning {
// updates["app_data"] = constant.StatusFailed
// }
if item.PanelData == constant.StatusRunning {
updates["panel_data"] = constant.StatusFailed
}

View file

@ -21,6 +21,7 @@ func Init() {
migrations.UpdateApp,
migrations.AddTaskDB,
migrations.UpdateAppInstall,
migrations.UpdateSnapshot,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -259,3 +259,10 @@ var UpdateAppInstall = &gormigrate.Migration{
&model.AppInstall{})
},
}
var UpdateSnapshot = &gormigrate.Migration{
ID: "20240913-update-snapshot",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Snapshot{}, &model.SnapshotStatus{})
},
}

View file

@ -15,6 +15,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.GET("/snapshot/load", baseApi.LoadSnapshotData)
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus)
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)

View file

@ -450,6 +450,48 @@ func (f FileOp) CopyDir(src, dst string) error {
return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/"))
}
func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error {
srcInfo, err := f.Fs.Stat(src)
if err != nil {
return err
}
dstDir := filepath.Join(dst, srcInfo.Name())
if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
if len(excludeNames) == 0 {
return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/"))
}
tmpFiles, err := os.ReadDir(src)
if err != nil {
return err
}
for _, item := range tmpFiles {
isExclude := false
for _, name := range excludeNames {
if item.Name() == name {
isExclude = true
break
}
}
if isExclude {
continue
}
if item.IsDir() {
fmt.Println(path.Join(src, item.Name()), dstDir)
if err := f.CopyDir(path.Join(src, item.Name()), dstDir); err != nil {
return err
}
continue
}
if err := f.CopyFile(path.Join(src, item.Name()), dstDir); err != nil {
return err
}
}
return nil
}
func (f FileOp) CopyFile(src, dst string) error {
dst = filepath.Clean(dst) + string(filepath.Separator)
return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dst+"/"))
@ -664,3 +706,56 @@ func ZipFile(files []archiver.File, dst afero.File) error {
}
return nil
}
func (f FileOp) TarGzCompressPro(withDir bool, src, dst, secret, exclusionRules string) error {
workdir := src
srcItem := "."
if withDir {
workdir = path.Dir(src)
srcItem = path.Base(src)
}
commands := ""
exMap := make(map[string]struct{})
exStr := ""
excludes := strings.Split(exclusionRules, ";")
excludes = append(excludes, "*.sock")
for _, exclude := range excludes {
if len(exclude) == 0 {
continue
}
if _, ok := exMap[exclude]; ok {
continue
}
exStr += " --exclude "
exStr += exclude
exMap[exclude] = struct{}{}
}
if len(secret) != 0 {
commands = fmt.Sprintf("tar -zcf - %s | openssl enc -aes-256-cbc -salt -pbkdf2 -k '%s' -out %s", srcItem, secret, dst)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar zcf %s %s %s", dst, exStr, srcItem)
global.LOG.Debug(commands)
}
return cmd.ExecCmdWithDir(commands, workdir)
}
func (f FileOp) TarGzExtractPro(src, dst string, secret string) error {
if _, err := os.Stat(path.Dir(dst)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(dst), os.ModePerm); err != nil {
return err
}
}
commands := ""
if len(secret) != 0 {
commands = fmt.Sprintf("openssl enc -d -aes-256-cbc -salt -pbkdf2 -k '%s' -in %s | tar -zxf - > /root/log", secret, src)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar zxvf %s", src)
global.LOG.Debug(commands)
}
return cmd.ExecCmdWithDir(commands, dst)
}

View file

@ -2,6 +2,8 @@ package files
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
@ -59,3 +61,56 @@ func (t TarGzArchiver) Compress(sourcePaths []string, dstFile string, secret str
}
return nil
}
func (t TarGzArchiver) CompressPro(withDir bool, src, dst, secret, exclusionRules string) error {
workdir := src
srcItem := "."
if withDir {
workdir = path.Dir(src)
srcItem = path.Base(src)
}
commands := ""
exMap := make(map[string]struct{})
exStr := ""
excludes := strings.Split(exclusionRules, ";")
excludes = append(excludes, "*.sock")
for _, exclude := range excludes {
if len(exclude) == 0 {
continue
}
if _, ok := exMap[exclude]; ok {
continue
}
exStr += " --exclude "
exStr += exclude
exMap[exclude] = struct{}{}
}
if len(secret) != 0 {
commands = fmt.Sprintf("tar -zcf - %s | openssl enc -aes-256-cbc -salt -pbkdf2 -k '%s' -out %s", srcItem, secret, dst)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar zcf %s %s %s", dst, exStr, srcItem)
global.LOG.Debug(commands)
}
return cmd.ExecCmdWithDir(commands, workdir)
}
func (t TarGzArchiver) ExtractPro(src, dst string, secret string) error {
if _, err := os.Stat(path.Dir(dst)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(dst), os.ModePerm); err != nil {
return err
}
}
commands := ""
if len(secret) != 0 {
commands = fmt.Sprintf("openssl enc -d -aes-256-cbc -salt -pbkdf2 -k '%s' -in %s | tar -zxf - > /root/log", secret, src)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar zxvf %s", src)
global.LOG.Debug(commands)
}
return cmd.ExecCmdWithDir(commands, dst)
}

View file

@ -119,11 +119,18 @@ export namespace Setting {
export interface SnapshotCreate {
id: number;
from: string;
fromAccounts: Array<string>;
defaultDownload: string;
sourceAccountIDs: string;
downloadAccountID: string;
description: string;
secret: string;
appData: Array<DataTree>;
panelData: Array<DataTree>;
backupData: Array<DataTree>;
withMonitorData: boolean;
withLoginLog: boolean;
withOperationLog: boolean;
}
export interface SnapshotImport {
from: string;
@ -155,11 +162,31 @@ export namespace Setting {
lastRollbackedAt: string;
secret: string;
}
export interface SnapshotData {
appData: Array<DataTree>;
panelData: Array<DataTree>;
backupData: Array<DataTree>;
withMonitorData: boolean;
withLoginLog: boolean;
withOperationLog: boolean;
}
export interface DataTree {
id: string;
label: string;
key: string;
name: string;
size: number;
isCheck: boolean;
isDisable: boolean;
path: string;
Children: Array<DataTree>;
}
export interface SnapshotStatus {
panel: string;
panelInfo: string;
daemonJson: string;
appData: string;
baseData: string;
appImage: string;
panelData: string;
backupData: string;

View file

@ -88,6 +88,9 @@ export const bindMFA = (param: Setting.MFABind) => {
};
// snapshot
export const loadSnapshotSetting = () => {
return http.get<Setting.SnapshotData>(`/settings/snapshot/load`);
};
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
return http.post(`/settings/snapshot`, param);
};

View file

@ -1585,6 +1585,36 @@ const message = {
'Backup files not in the current backup list, please try downloading from the file directory and importing for backup.',
snapshot: 'Snapshot',
stepBaseData: 'Base Data',
stepAppData: 'System Application',
stepPanelData: 'System Data',
stepBackupData: 'Backup Data',
stepOtherData: 'Other Data',
loginLog: 'Backup System Login Records',
OperationLog: 'Backup System Operation Log Login Records',
monitorData: 'Backup System Monitoring Data',
selectAllImage: 'Backup All Application Images',
agentLabel: 'Node Configuration',
appDataLabel: 'Application Data',
appImage: 'Application Image',
appBackup: 'Application Backup',
backupLabel: 'Backup Directory',
cacheLabel: 'Cache Directory',
confLabel: 'Configuration File',
dbLabel: 'Database Directory',
dockerLabel: 'Container Related Logs',
logLabel: 'Log Directory',
taskLabel: 'Scheduled Task Report',
resourceLabel: 'Application Resource Directory',
runtimeLabel: 'Runtime Directory',
appLabel: 'Application',
databaseLabel: 'Database',
websiteLabel: 'Website',
directoryLabel: 'Directory',
appStoreLabel: 'Application Store',
shellLabel: 'Script',
tmpLabel: 'Temporary Directory',
sslLabel: 'Certificate Directory',
deleteHelper:
'All backup files for the snapshot, including those in the third-party backup account, will be deleted.',
status: 'Snapshot status',

View file

@ -1403,6 +1403,36 @@ const message = {
backupJump: '未在當前備份列表中的備份檔案請嘗試從檔案目錄中下載後導入備份',
snapshot: '快照',
stepBaseData: '基礎數據',
stepAppData: '系統應用',
stepPanelData: '系統數據',
stepBackupData: '備份數據',
stepOtherData: '其他數據',
loginLog: '備份系統登錄記錄',
OperationLog: '備份系統操作日誌登錄記錄',
monitorData: '備份系統監控數據',
selectAllImage: '備份所有應用鏡像',
agentLabel: '節點配置',
appDataLabel: '應用數據',
appImage: '應用鏡像',
appBackup: '應用備份',
backupLabel: '備份目錄',
cacheLabel: '緩存目錄',
confLabel: '配置文件',
dbLabel: '數據庫目錄',
dockerLabel: '容器相關日誌',
logLabel: '日誌目錄',
taskLabel: '計劃任務報告',
resourceLabel: '應用資源目錄',
runtimeLabel: '運行時目錄',
appLabel: '應用',
databaseLabel: '數據庫',
websiteLabel: '網站',
directoryLabel: '目錄',
appStoreLabel: '應用商店',
shellLabel: '腳本',
tmpLabel: '臨時目錄',
sslLabel: '證書目錄',
deleteHelper: '將刪除該快照的所有備份文件包括第三方備份賬號中的文件',
status: '快照狀態',
ignoreRule: '排除規則',

View file

@ -1405,6 +1405,36 @@ const message = {
backupJump: '未在当前备份列表中的备份文件请尝试从文件目录中下载后导入备份',
snapshot: '快照',
stepBaseData: '基础数据',
stepAppData: '系统应用',
stepPanelData: '系统数据',
stepBackupData: '备份数据',
stepOtherData: '其他数据',
loginLog: '备份系统登陆记录',
OperationLog: '备份系统备份系统操作日志登陆记录',
monitorData: '备份系统监控数据',
selectAllImage: '备份所有应用镜像',
agentLabel: '节点配置',
appDataLabel: '应用数据',
appImage: '应用镜像',
appBackup: '应用备份',
backupLabel: '备份目录',
cacheLabel: '缓存目录',
confLabel: '配置文件',
dbLabel: '数据库目录',
dockerLabel: '容器相关日志',
logLabel: '日志目录',
taskLabel: '计划任务报告',
resourceLabel: '应用资源目录',
runtimeLabel: '运行时目录',
appLabel: '应用',
databaseLabel: '数据库',
websiteLabel: '网站',
directoryLabel: '目录',
appStoreLabel: '应用商店',
shellLabel: '脚本',
tmpLabel: '临时目录',
sslLabel: '证书目录',
deleteHelper: '将删除该快照的所有备份文件包括第三方备份账号中的文件',
ignoreRule: '排除规则',
ignoreHelper: '快照时将使用该规则对 1Panel 数据目录进行压缩备份请谨慎修改',

View file

@ -0,0 +1,485 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('setting.snapshot')" :back="handleClose" size="large">
<fu-steps
v-loading="loading"
direction="vertical"
class="steps"
:space="50"
ref="stepsRef"
:isLoading="stepLoading"
:finishButtonText="$t('commons.button.create')"
@finish="submitAddSnapshot"
@change="changeStep"
:beforeLeave="beforeLeave"
>
<fu-step id="baseData" :title="$t('setting.stepBaseData')">
<el-form
v-loading="loading"
class="mt-5"
label-position="top"
ref="formRef"
:model="form"
:rules="rules"
>
<el-form-item :label="$t('setting.backupAccount')" prop="fromAccounts">
<el-select multiple @change="changeAccount" v-model="form.fromAccounts" clearable>
<div v-for="item in backupOptions" :key="item.id">
<el-option
v-if="item.type !== $t('setting.LOCAL')"
:value="item.id"
:label="item.type + ' - ' + item.name"
/>
<el-option v-else :value="item.id" :label="item.type" />
</div>
</el-select>
</el-form-item>
<el-form-item :label="$t('cronjob.default_download_path')" prop="downloadAccountID">
<el-select v-model="form.downloadAccountID" clearable>
<div v-for="item in accountOptions" :key="item.id">
<el-option
v-if="item.type !== $t('setting.LOCAL')"
:value="item.id"
:label="item.type + ' - ' + item.name"
/>
<el-option v-else :value="item.id" :label="item.type" />
</div>
</el-select>
</el-form-item>
<el-form-item :label="$t('setting.compressPassword')" prop="secret">
<el-input v-model="form.secret"></el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" clearable v-model="form.description" />
</el-form-item>
</el-form>
</fu-step>
<fu-step id="appData" :title="$t('setting.stepAppData')">
<div>
<el-checkbox
class="ml-6"
v-model="form.backupAllImage"
@change="selectAllImage"
:label="$t('setting.selectAllImage')"
size="large"
/>
<el-tree
style="max-width: 600px"
ref="appRef"
node-key="id"
:default-expand-all="true"
:data="form.appData"
:props="defaultProps"
show-checkbox
>
<template #default="{ data }">
<div class="float-left">
<span>{{ loadApp18n(data.label) }}</span>
</div>
<div class="ml-4 float-left">
<span v-if="data.size">{{ computeSize(data.size) }}</span>
</div>
</template>
</el-tree>
</div>
</fu-step>
<fu-step id="panelData" :title="$t('setting.stepPanelData')">
<div>
<el-tree
style="max-width: 600px"
ref="panelRef"
node-key="id"
:data="form.panelData"
:props="defaultProps"
show-checkbox
>
<template #default="{ node, data }">
<div class="float-left">
<span>{{ load18n(node, data.label) }}</span>
</div>
<div class="ml-4 float-left">
<span v-if="data.size">{{ computeSize(data.size) }}</span>
</div>
</template>
</el-tree>
</div>
</fu-step>
<fu-step id="backupData" :title="$t('setting.stepBackupData')">
<div>
<el-tree
style="max-width: 600px"
ref="backupRef"
node-key="id"
:data="form.backupData"
:props="defaultProps"
show-checkbox
>
<template #default="{ node, data }">
<div class="float-left">
<span>{{ load18n(node, data.label) }}</span>
</div>
<div class="ml-4 float-left">
<span v-if="data.size">{{ computeSize(data.size) }}</span>
</div>
</template>
</el-tree>
</div>
</fu-step>
<fu-step id="otherData" :title="$t('setting.stepOtherData')">
<div class="ml-6">
<el-checkbox v-model="form.withLoginLog" :label="$t('setting.loginLog')" size="large" />
</div>
<div class="ml-6">
<el-checkbox v-model="form.withOperationLog" :label="$t('setting.OperationLog')" size="large" />
</div>
<div class="ml-6">
<el-checkbox v-model="form.withMonitorData" :label="$t('setting.monitorData')" size="large" />
</div>
</fu-step>
</fu-steps>
<template #footer>
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { loadSnapshotSetting, snapshotCreate } from '@/api/modules/setting';
import { computeSize } from '@/utils/util';
import i18n from '@/lang';
import { getBackupList } from '@/api/modules/backup';
import { Rules } from '@/global/form-rules';
import { ElForm } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
const loading = ref();
const stepLoading = ref(false);
const stepsRef = ref();
const appRef = ref();
const panelRef = ref();
const backupRef = ref();
const backupOptions = ref();
const accountOptions = ref();
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const form = reactive({
id: 0,
downloadAccountID: '',
fromAccounts: [],
sourceAccountIDs: '',
description: '',
secret: '',
backupAllImage: false,
withLoginLog: false,
withOperationLog: false,
withMonitorData: false,
panelData: [],
backupData: [],
appData: [],
});
const rules = reactive({
fromAccounts: [Rules.requiredSelect],
downloadAccountID: [Rules.requiredSelect],
});
const defaultProps = {
children: 'children',
label: 'label',
checked: 'isCheck',
disabled: 'isDisable',
};
const drawerVisible = ref();
const emit = defineEmits(['search']);
const acceptParams = (): void => {
search();
loadBackups();
drawerVisible.value = true;
};
const handleClose = () => {
drawerVisible.value = false;
};
const submitForm = async (formEl: any) => {
let bool;
if (!formEl) return;
await formEl.validate((valid: boolean) => {
if (valid) {
bool = true;
} else {
bool = false;
}
});
return bool;
};
const beforeLeave = async (stepItem: any) => {
switch (stepItem.id) {
case 'baseData':
if (await submitForm(formRef.value)) {
stepsRef.value.next();
return true;
} else {
return false;
}
case 'appData':
let appChecks = appRef.value.getCheckedNodes();
loadCheckForSubmit(appChecks, form.appData);
return true;
case 'panelData':
let panelChecks = panelRef.value.getCheckedNodes();
loadCheckForSubmit(panelChecks, form.panelData);
return true;
case 'backupData':
let backupChecks = backupRef.value.getCheckedNodes();
loadCheckForSubmit(backupChecks, form.backupData);
return true;
}
};
const loadApp18n = (label: string) => {
switch (label) {
case 'appData':
return i18n.global.t('setting.appDataLabel');
case 'appImage':
case 'appBackup':
return i18n.global.t('setting.' + label);
default:
return label;
}
};
const loadBackups = async () => {
const res = await getBackupList();
backupOptions.value = [];
for (const item of res.data) {
if (item.id !== 0) {
backupOptions.value.push({ id: item.id, type: i18n.global.t('setting.' + item.type), name: item.name });
}
}
changeAccount();
};
const changeStep = (currentStep: any) => {
switch (currentStep.id) {
case 'appData':
if (appRef.value) {
return;
}
nextTick(() => {
setAppDefaultCheck(form.appData);
});
return;
case 'panelData':
if (panelRef.value) {
return;
}
nextTick(() => {
setPanelDefaultCheck(form.panelData);
return;
});
return;
case 'backupData':
if (backupRef.value) {
return;
}
nextTick(() => {
setBackupDefaultCheck(form.backupData);
return;
});
return;
}
};
const changeAccount = async () => {
accountOptions.value = [];
let isInAccounts = false;
for (const item of backupOptions.value) {
let exist = false;
for (const ac of form.fromAccounts) {
if (item.id == ac) {
exist = true;
break;
}
}
if (exist) {
if (item.id === form.downloadAccountID) {
isInAccounts = true;
}
accountOptions.value.push(item);
}
}
if (!isInAccounts) {
form.downloadAccountID = form.downloadAccountID ? undefined : form.downloadAccountID;
}
};
const load18n = (node: any, label: string) => {
if (node.level === 1) {
switch (label) {
case 'agent':
case 'conf':
case 'db':
case 'docker':
case 'log':
case 'runtime':
case 'task':
case 'app':
case 'database':
case 'website':
case 'directory':
return i18n.global.t('setting.' + label + 'Label');
default:
return label;
}
}
if (node.level === 2) {
switch (label) {
case 'App':
return i18n.global.t('setting.appLabel');
case 'AppStore':
return i18n.global.t('setting.appStoreLabel');
case 'shell':
return i18n.global.t('setting.shellLabel');
default:
return label;
}
}
return label;
};
const submitAddSnapshot = async () => {
loading.value = true;
form.sourceAccountIDs = form.fromAccounts.join(',');
await snapshotCreate(form)
.then(() => {
loading.value = false;
drawerVisible.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
const loadCheckForSubmit = (checks: any, list: any) => {
for (const item of list) {
let isCheck = false;
for (const check of checks) {
if (item.id == check.id) {
isCheck = true;
break;
}
}
item.isCheck = isCheck;
if (item.children) {
loadCheckForSubmit(checks, item.children);
}
}
};
const selectAllImage = () => {
for (const item of form.appData) {
for (const item2 of item.children) {
if (item2.label === 'appImage') {
appRef.value.setChecked(item2.id, form.backupAllImage, false);
}
}
}
};
const search = async () => {
const res = await loadSnapshotSetting();
form.panelData = res.data.panelData || [];
form.backupData = res.data.backupData || [];
form.appData = res.data.appData || [];
};
const setAppDefaultCheck = async (list: any) => {
for (const item of list) {
if (item.isCheck) {
appRef.value.setChecked(item.id, true, true);
continue;
}
if (item.children) {
setAppDefaultCheck(item.children);
}
}
};
const setPanelDefaultCheck = async (list: any) => {
for (const item of list) {
if (item.isCheck) {
panelRef.value.setChecked(item.id, true, true);
continue;
}
if (item.children) {
setPanelDefaultCheck(item.children);
}
}
};
const setBackupDefaultCheck = async (list: any) => {
for (const item of list) {
if (item.isCheck) {
backupRef.value.setChecked(item.id, true, true);
continue;
}
if (item.children) {
setBackupDefaultCheck(item.children);
}
}
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.steps {
width: 100%;
margin-top: 20px;
:deep(.el-step) {
.el-step__line {
background-color: var(--el-color-primary-light-5);
}
.el-step__head.is-success {
color: var(--el-color-primary-light-5);
border-color: var(--el-color-primary-light-5);
}
.el-step__icon {
color: var(--el-color-primary-light-2);
}
.el-step__icon.is-text {
border-radius: 50%;
border: 2px solid;
border-color: var(--el-color-primary-light-2);
}
.el-step__title.is-finish {
color: #717379;
font-size: 13px;
font-weight: bold;
}
.el-step__description.is-finish {
color: #606266;
}
.el-step__title.is-success {
font-weight: bold;
color: var(--el-color-primary-light-2);
}
.el-step__title.is-process {
font-weight: bold;
color: var(--el-text-color-regular);
}
}
}
</style>

View file

@ -88,7 +88,7 @@
<el-button v-if="row.status === 'Failed'" type="danger" @click="onLoadStatus(row)" link>
{{ $t('commons.status.error') }}
</el-button>
<el-tag v-if="row.status === 'Success'" type="success">
<el-tag v-if="row.status === 'Success'" @click="onLoadStatus(row)" type="success">
{{ $t('commons.status.success') }}
</el-tag>
</template>
@ -115,52 +115,8 @@
</template>
</LayoutContent>
<RecoverStatus ref="recoverStatusRef" @search="search()"></RecoverStatus>
<SnapshotCreate ref="createRef" @search="search()" />
<SnapshotImport ref="importRef" @search="search()" />
<DrawerPro v-model="drawerVisible" :header="$t('setting.createSnapshot')" :back="handleClose" size="large">
<el-form
v-loading="loading"
label-position="top"
ref="snapRef"
label-width="100px"
:model="snapInfo"
:rules="rules"
>
<el-form-item :label="$t('setting.backupAccount')" prop="fromAccounts">
<el-select multiple @change="changeAccount" v-model="snapInfo.fromAccounts" clearable>
<el-option
v-for="item in backupOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('cronjob.default_download_path')" prop="defaultDownload">
<el-select v-model="snapInfo.defaultDownload" clearable>
<el-option
v-for="item in accountOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('setting.compressPassword')" prop="secret">
<el-input v-model="snapInfo.secret"></el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" clearable v-model="snapInfo.description" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="loading" @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="submitAddSnapshot(snapRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DrawerPro>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
<template #content>
@ -180,18 +136,17 @@
</template>
<script setup lang="ts">
import { snapshotCreate, searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
import { searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
import { onMounted, reactive, ref } from 'vue';
import { computeSize, dateFormat } from '@/utils/util';
import { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rules';
import IgnoreRule from '@/views/setting/snapshot/ignore-rule/index.vue';
import i18n from '@/lang';
import { Setting } from '@/api/interface/setting';
import IgnoreRule from '@/views/setting/snapshot/ignore-rule/index.vue';
import SnapStatus from '@/views/setting/snapshot/snap_status/index.vue';
import RecoverStatus from '@/views/setting/snapshot/status/index.vue';
import SnapshotImport from '@/views/setting/snapshot/import/index.vue';
import { getBackupList } from '@/api/modules/backup';
import SnapshotCreate from '@/views/setting/snapshot/create/index.vue';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
@ -208,39 +163,15 @@ const searchName = ref();
const opRef = ref();
const ignoreRef = ref();
const createRef = ref();
const snapStatusRef = ref();
const recoverStatusRef = ref();
const importRef = ref();
const isRecordShow = ref();
const backupOptions = ref();
const accountOptions = ref();
const operateIDs = ref();
type FormInstance = InstanceType<typeof ElForm>;
const snapRef = ref<FormInstance>();
const rules = reactive({
fromAccounts: [Rules.requiredSelect],
defaultDownload: [Rules.requiredSelect],
});
let snapInfo = reactive<Setting.SnapshotCreate>({
id: 0,
from: '',
defaultDownload: '',
fromAccounts: [],
description: '',
secret: '',
});
const cleanData = ref();
const drawerVisible = ref<boolean>(false);
const onCreate = async () => {
restForm();
drawerVisible.value = true;
};
const onImport = () => {
let names = [];
for (const item of data.value) {
@ -249,12 +180,12 @@ const onImport = () => {
importRef.value.acceptParams({ names: names });
};
const onIgnore = () => {
ignoreRef.value.acceptParams();
const onCreate = () => {
createRef.value.acceptParams();
};
const handleClose = () => {
drawerVisible.value = false;
const onIgnore = () => {
ignoreRef.value.acceptParams();
};
const onChange = async (info: any) => {
@ -262,25 +193,6 @@ const onChange = async (info: any) => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const submitAddSnapshot = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
snapInfo.from = snapInfo.fromAccounts.join(',');
await snapshotCreate(snapInfo)
.then(() => {
loading.value = false;
drawerVisible.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const onLoadStatus = (row: Setting.SnapshotInfo) => {
snapStatusRef.value.acceptParams({
id: row.id,
@ -290,40 +202,6 @@ const onLoadStatus = (row: Setting.SnapshotInfo) => {
});
};
const loadBackups = async () => {
const res = await getBackupList();
backupOptions.value = [];
for (const item of res.data) {
if (item.id !== 0) {
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
}
}
changeAccount();
};
const changeAccount = async () => {
accountOptions.value = [];
let isInAccounts = false;
for (const item of backupOptions.value) {
let exist = false;
for (const ac of snapInfo.fromAccounts) {
if (item.value == ac) {
exist = true;
break;
}
}
if (exist) {
if (item.value === snapInfo.defaultDownload) {
isInAccounts = true;
}
accountOptions.value.push(item);
}
}
if (!isInAccounts) {
snapInfo.defaultDownload = '';
}
};
const batchDelete = async (row: Setting.SnapshotInfo | null) => {
let names = [];
let ids = [];
@ -362,11 +240,6 @@ const onSubmitDelete = async () => {
});
};
function restForm() {
if (snapRef.value) {
snapRef.value.resetFields();
}
}
const buttons = [
{
label: i18n.global.t('commons.button.recover'),
@ -406,6 +279,5 @@ const search = async () => {
onMounted(() => {
search();
loadBackups();
});
</script>

View file

@ -1,220 +1,295 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('setting.recoverDetail')" :back="handleClose" size="small">
<el-form label-position="top" v-loading="loading">
<span class="card-title">{{ $t('setting.recover') }}</span>
<el-divider class="divider" />
<div v-if="!snapInfo.recoverStatus && !snapInfo.lastRecoveredAt">
<el-alert center class="alert" style="height: 257px" :closable="false">
<el-button size="large" round plain type="primary" @click="recoverSnapshot(true)">
{{ $t('setting.recover') }}
<el-dialog
v-model="dialogVisible"
@close="onClose"
:destroy-on-close="true"
:close-on-click-modal="false"
width="50%"
>
<template #header>
<div class="card-header">
<span>{{ $t('setting.status') }}</span>
</div>
</template>
<div v-loading="loading">
<el-alert :type="loadStatus(status.baseData)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.baseData)" link>{{ $t('setting.baseData') }}</el-button>
<div v-if="showErrorMsg(status.baseData)" class="top-margin">
<span class="err-message">{{ status.baseData }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.appImage)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.appImage)" link>{{ $t('setting.appData') }}</el-button>
<div v-if="showErrorMsg(status.appImage)" class="top-margin">
<span class="err-message">{{ status.appImage }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.backupData)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.backupData)" link>{{ $t('setting.backupData') }}</el-button>
<div v-if="showErrorMsg(status.backupData)" class="top-margin">
<span class="err-message">{{ status.backupData }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.panelData)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.panelData)" link>{{ $t('setting.panelData') }}</el-button>
<div v-if="showErrorMsg(status.panelData)" class="top-margin">
<span class="err-message">{{ status.panelData }}</span>
</div>
</template>
</el-alert>
<el-alert :type="loadStatus(status.compress)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.compress)" link>
{{ $t('setting.compress') }} {{ status.size }}
</el-button>
</el-alert>
</div>
<el-card v-else class="mini-border-card">
<div v-if="!snapInfo.recoverStatus" class="mini-border-card">
<div v-if="snapInfo.lastRecoveredAt">
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-button @click="recoverSnapshot(true)" style="margin-left: 10px" type="primary">
{{ $t('setting.recover') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRecoverAt')">
{{ snapInfo.lastRecoveredAt }}
</el-form-item>
<div v-if="showErrorMsg(status.compress)" class="top-margin">
<span class="err-message">{{ status.compress }}</span>
</div>
</div>
<div v-else>
<el-form-item :label="$t('commons.table.status')">
<el-tag type="danger" v-if="snapInfo.recoverStatus === 'Failed'">
{{ $t('commons.table.statusFailed') }}
</el-tag>
<el-tag type="success" v-if="snapInfo.recoverStatus === 'Success'">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tag type="info" v-if="snapInfo.recoverStatus === 'Waiting'">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
</el-form-item>
<el-form-item :label="$t('setting.lastRecoverAt')" v-if="snapInfo.recoverStatus !== 'Waiting'">
{{ snapInfo.lastRecoveredAt }}
</el-form-item>
<div v-if="snapInfo.recoverStatus === 'Failed'">
<el-form-item :label="$t('commons.button.log')">
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
{{ snapInfo.recoverMessage }}
</span>
</el-form-item>
<el-form-item>
<el-button @click="recoverSnapshot(false)" type="primary">
{{ $t('commons.button.retry') }}
</el-button>
</el-form-item>
</template>
</el-alert>
<el-alert :type="loadStatus(status.upload)" :closable="false">
<template #title>
<el-button :icon="loadIcon(status.upload)" link>
{{ $t('setting.upload') }}
</el-button>
<div v-if="showErrorMsg(status.upload)" class="top-margin">
<span class="err-message">{{ status.upload }}</span>
</div>
</div>
</el-card>
<div v-if="snapInfo.recoverStatus === 'Failed'">
<span class="card-title">{{ $t('setting.rollback') }}</span>
<el-divider class="divider" />
<div v-if="!snapInfo.rollbackStatus && !snapInfo.lastRollbackedAt">
<el-alert center class="alert" style="height: 257px" :closable="false">
<el-button size="large" round plain type="primary" @click="rollbackSnapshot()">
{{ $t('setting.rollback') }}
</el-button>
</el-alert>
</div>
<div v-if="!snapInfo.rollbackStatus">
<div v-if="snapInfo.lastRollbackedAt">
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-button @click="rollbackSnapshot" style="margin-left: 10px" type="primary">
{{ $t('setting.rollback') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRollbackAt')">
{{ snapInfo.lastRollbackedAt }}
</el-form-item>
</div>
</div>
<div v-else>
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success" v-if="snapInfo.rollbackStatus === 'Success'">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tag type="danger" v-if="snapInfo.rollbackStatus === 'Failed'">
{{ $t('commons.table.statusFailed') }}
</el-tag>
<el-tag type="info" v-if="snapInfo.rollbackStatus === 'Waiting'">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
<el-button
style="margin-left: 15px"
:disabled="snapInfo.rollbackStatus !== 'Success'"
@click="rollbackSnapshot"
>
{{ $t('setting.rollback') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRollbackAt')" v-if="snapInfo.rollbackStatus !== 'Waiting'">
{{ snapInfo.lastRollbackedAt }}
</el-form-item>
<div v-if="snapInfo.rollbackStatus === 'Failed'">
<el-form-item :label="$t('commons.button.log')">
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
{{ snapInfo.rollbackMessage }}
</span>
</el-form-item>
<el-form-item>
<el-button @click="rollbackSnapshot()" type="primary">
{{ $t('commons.button.retry') }}
</el-button>
</el-form-item>
</div>
</div>
</div>
</el-form>
</DrawerPro>
<SnapRecover ref="recoverRef" @close="handleClose" />
</template>
</el-alert>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="onClose">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button v-if="showRetry()" @click="onRetry">
{{ $t('commons.button.retry') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
<script lang="ts" setup>
import { Setting } from '@/api/interface/setting';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { snapshotRollback } from '@/api/modules/setting';
import { MsgSuccess } from '@/utils/message';
import { loadOsInfo } from '@/api/modules/dashboard';
import SnapRecover from '@/views/setting/snapshot/recover/index.vue';
import { loadSnapStatus, snapshotCreate } from '@/api/modules/setting';
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
const status = reactive<Setting.SnapshotStatus>({
baseData: '',
appImage: '',
panelData: '',
backupData: '',
compress: '',
size: '',
upload: '',
});
const dialogVisible = ref(false);
const drawerVisible = ref(false);
const snapInfo = ref();
const loading = ref();
const snapID = ref();
const snapFrom = ref();
const snapDefaultDownload = ref();
const snapDescription = ref();
const recoverRef = ref();
let timer: NodeJS.Timer | null = null;
interface DialogProps {
snapInfo: Setting.SnapshotInfo;
id: number;
from: string;
defaultDownload: string;
description: string;
}
const acceptParams = (params: DialogProps): void => {
snapInfo.value = params.snapInfo;
drawerVisible.value = true;
const acceptParams = (props: DialogProps): void => {
dialogVisible.value = true;
snapID.value = props.id;
snapFrom.value = props.from;
snapDefaultDownload.value = props.defaultDownload;
snapDescription.value = props.description;
onWatch();
nextTick(() => {
loadCurrentStatus();
});
};
const emit = defineEmits(['search']);
const handleClose = () => {
drawerVisible.value = false;
};
const recoverSnapshot = async (isNew: boolean) => {
const loadCurrentStatus = async () => {
loading.value = true;
await loadOsInfo()
await loadSnapStatus(snapID.value)
.then((res) => {
loading.value = false;
let params = {
id: snapInfo.value.id,
isNew: isNew,
name: snapInfo.value.name,
reDownload: false,
secret: snapInfo.value.secret,
status.baseData = res.data.baseData;
status.appImage = res.data.appImage;
status.panelData = res.data.panelData;
status.backupData = res.data.backupData;
arch: res.data.kernelArch,
size: snapInfo.value.size,
freeSize: res.data.diskSize,
};
recoverRef.value.acceptParams(params);
status.compress = res.data.compress;
status.size = res.data.size;
status.upload = res.data.upload;
})
.catch(() => {
loading.value = false;
});
};
const rollbackSnapshot = async () => {
ElMessageBox.confirm(i18n.global.t('setting.rollbackHelper'), i18n.global.t('setting.rollback'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await snapshotRollback({ id: snapInfo.value.id, isNew: false, reDownload: false, secret: '' })
.then(() => {
emit('search');
loading.value = false;
drawerVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
const onClose = async () => {
emit('search');
dialogVisible.value = false;
};
const onRetry = async () => {
loading.value = true;
await snapshotCreate({
id: snapID.value,
description: snapDescription.value,
downloadAccountID: '',
sourceAccountIDs: '',
secret: '',
withLoginLog: false,
withOperationLog: false,
withMonitorData: false,
panelData: [],
backupData: [],
appData: [],
})
.then(() => {
loading.value = false;
loadCurrentStatus();
})
.catch(() => {
loading.value = false;
});
};
const onWatch = () => {
timer = setInterval(async () => {
if (keepLoadStatus()) {
const res = await loadSnapStatus(snapID.value);
status.baseData = res.data.baseData;
status.appImage = res.data.appImage;
status.panelData = res.data.panelData;
status.backupData = res.data.backupData;
status.compress = res.data.compress;
status.size = res.data.size;
status.upload = res.data.upload;
}
}, 1000 * 3);
};
const keepLoadStatus = () => {
if (status.baseData === 'Running') {
return true;
}
if (status.appImage === 'Running') {
return true;
}
if (status.panelData === 'Running') {
return true;
}
if (status.backupData === 'Running') {
return true;
}
if (status.compress === 'Running') {
return true;
}
if (status.upload === 'Uploading') {
return true;
}
return false;
};
const showErrorMsg = (status: string) => {
return status !== 'Running' && status !== 'Done' && status !== 'Uploading' && status !== 'Waiting';
};
const showRetry = () => {
if (keepLoadStatus()) {
return false;
}
if (status.baseData !== 'Running' && status.baseData !== 'Done') {
return true;
}
if (status.appImage !== 'Running' && status.appImage !== 'Done') {
return true;
}
if (status.panelData !== 'Running' && status.panelData !== 'Done') {
return true;
}
if (status.backupData !== 'Running' && status.backupData !== 'Done') {
return true;
}
if (status.compress !== 'Running' && status.compress !== 'Done' && status.compress !== 'Waiting') {
return true;
}
if (status.upload !== 'Uploading' && status.upload !== 'Done' && status.upload !== 'Waiting') {
return true;
}
return false;
};
const loadStatus = (status: string) => {
switch (status) {
case 'Running':
case 'Waiting':
case 'Uploading':
return 'info';
case 'Done':
return 'success';
default:
return 'error';
}
};
const loadIcon = (status: string) => {
switch (status) {
case 'Running':
case 'Waiting':
case 'Uploading':
return 'Loading';
case 'Done':
return 'Check';
default:
return 'Close';
}
};
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
});
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.divider {
display: block;
height: 1px;
width: 100%;
margin: 12px 0;
border-top: 1px var(--el-border-color) var(--el-border-style);
<style scoped lang="scss">
.el-alert {
margin: 10px 0 0;
}
.alert {
background-color: rgba(0, 94, 235, 0.03);
.el-alert:first-child {
margin: 0;
}
.card-title {
font-size: 14px;
font-weight: 500;
line-height: 25px;
color: var(--el-button-text-color, var(--el-text-color-regular));
.top-margin {
margin-top: 10px;
}
.err-message {
margin-left: 23px;
line-height: 20px;
word-break: break-all;
word-wrap: break-word;
}
</style>