mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-06 23:24:23 +08:00
This commit is contained in:
parent
7add6ab190
commit
f8431c787f
38 changed files with 2591 additions and 1495 deletions
|
|
@ -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 创建系统快照
|
||||
|
|
@ -29,6 +44,28 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
|
|||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Recreate system snapshot
|
||||
// @Description 创建系统快照重试
|
||||
// @Accept json
|
||||
// @Param request body dto.OperateByID true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/snapshot/recrete [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"重试创建快照 [name]","formatEN":recrete the snapshot [name]"}
|
||||
func (b *BaseApi) RecreateSnapshot(c *gin.Context) {
|
||||
var req dto.OperateByID
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := snapshotService.SnapshotReCreate(req.ID); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Import system snapshot
|
||||
// @Description 导入已有快照
|
||||
|
|
@ -51,28 +88,6 @@ func (b *BaseApi) ImportSnapshot(c *gin.Context) {
|
|||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load Snapshot status
|
||||
// @Description 获取快照状态
|
||||
// @Accept json
|
||||
// @Param request body dto.OperateByID true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/snapshot/status [post]
|
||||
func (b *BaseApi) LoadSnapShotStatus(c *gin.Context) {
|
||||
var req dto.OperateByID
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := snapshotService.LoadSnapShotStatus(req.ID)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Update snapshot description
|
||||
// @Description 更新快照描述信息
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
103
agent/app/dto/snapshot.go
Normal file
103
agent/app/dto/snapshot.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
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"`
|
||||
Name string `json:"name"`
|
||||
TaskID string `json:"taskID"`
|
||||
SourceAccountIDs string `json:"sourceAccountIDs" validate:"required"`
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Secret string `json:"secret"`
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
|
||||
AppData []DataTree `json:"appData"`
|
||||
BackupData []DataTree `json:"backupData"`
|
||||
PanelData []DataTree `json:"panelData"`
|
||||
|
||||
WithMonitorData bool `json:"withMonitorData"`
|
||||
WithLoginLog bool `json:"withLoginLog"`
|
||||
WithOperationLog bool `json:"withOperationLog"`
|
||||
WithSystemLog bool `json:"withSystemLog"`
|
||||
WithTaskLog bool `json:"withTaskLog"`
|
||||
}
|
||||
|
||||
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"`
|
||||
WithSystemLog bool `json:"withSystemLog"`
|
||||
WithTaskLog bool `json:"withTaskLog"`
|
||||
}
|
||||
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"`
|
||||
|
||||
RelationItemID string `json:"relationItemID"`
|
||||
Children []DataTree `json:"children"`
|
||||
}
|
||||
type SnapshotRecover struct {
|
||||
IsNew bool `json:"isNew"`
|
||||
ReDownload bool `json:"reDownload"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
TaskID string `json:"taskID"`
|
||||
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"`
|
||||
|
||||
TaskID string `json:"taskID"`
|
||||
TaskRecoverID string `json:"taskRecoverID"`
|
||||
TaskRollbackID string `json:"taskRollbackID"`
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package model
|
|||
type Snapshot struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;unique"`
|
||||
Secret string `json:"secret"`
|
||||
Description string `json:"description"`
|
||||
SourceAccountIDs string `json:"sourceAccountIDs"`
|
||||
DownloadAccountID uint `json:"downloadAccountID"`
|
||||
|
|
@ -10,26 +11,22 @@ type Snapshot struct {
|
|||
Message string `json:"message"`
|
||||
Version string `json:"version"`
|
||||
|
||||
TaskID string `json:"taskID"`
|
||||
TaskRecoverID string `json:"taskRecoverID"`
|
||||
TaskRollbackID string `json:"taskRollbackID"`
|
||||
|
||||
AppData string `json:"appData"`
|
||||
PanelData string `json:"panelData"`
|
||||
BackupData string `json:"backupData"`
|
||||
WithMonitorData bool `json:"withMonitorData"`
|
||||
WithLoginLog bool `json:"withLoginLog"`
|
||||
WithOperationLog bool `json:"withOperationLog"`
|
||||
WithSystemLog bool `json:"withSystemLog"`
|
||||
WithTaskLog bool `json:"withTaskLog"`
|
||||
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
RecoverStatus string `json:"recoverStatus"`
|
||||
RecoverMessage string `json:"recoverMessage"`
|
||||
LastRecoveredAt string `json:"lastRecoveredAt"`
|
||||
RollbackStatus string `json:"rollbackStatus"`
|
||||
RollbackMessage string `json:"rollbackMessage"`
|
||||
LastRollbackAt string `json:"lastRollbackAt"`
|
||||
}
|
||||
|
||||
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"`
|
||||
PanelData string `json:"panelData" gorm:"default:Running"`
|
||||
BackupData string `json:"backupData" gorm:"default:Running"`
|
||||
|
||||
Compress string `json:"compress" gorm:"default:Waiting"`
|
||||
Size string `json:"size" `
|
||||
Upload string `json:"upload" gorm:"default:Waiting"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,6 @@ type ISnapshotRepo interface {
|
|||
Update(id uint, vars map[string]interface{}) error
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error)
|
||||
Delete(opts ...DBOption) error
|
||||
|
||||
GetStatus(snapID uint) (model.SnapshotStatus, error)
|
||||
GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error)
|
||||
CreateStatus(snap *model.SnapshotStatus) error
|
||||
DeleteStatus(snapID uint) error
|
||||
UpdateStatus(id uint, vars map[string]interface{}) error
|
||||
}
|
||||
|
||||
func NewISnapshotRepo() ISnapshotRepo {
|
||||
|
|
@ -73,33 +67,3 @@ func (u *SnapshotRepo) Delete(opts ...DBOption) error {
|
|||
}
|
||||
return db.Delete(&model.Snapshot{}).Error
|
||||
}
|
||||
|
||||
func (u *SnapshotRepo) GetStatus(snapID uint) (model.SnapshotStatus, error) {
|
||||
var data model.SnapshotStatus
|
||||
if err := global.DB.Where("snap_id = ?", snapID).First(&data).Error; err != nil {
|
||||
return data, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (u *SnapshotRepo) GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error) {
|
||||
var status []model.SnapshotStatus
|
||||
db := global.DB.Model(&model.SnapshotStatus{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&status).Error
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (u *SnapshotRepo) CreateStatus(snap *model.SnapshotStatus) error {
|
||||
return global.DB.Create(snap).Error
|
||||
}
|
||||
|
||||
func (u *SnapshotRepo) DeleteStatus(snapID uint) error {
|
||||
return global.DB.Where("snap_id = ?", snapID).Delete(&model.SnapshotStatus{}).Error
|
||||
}
|
||||
|
||||
func (u *SnapshotRepo) UpdateStatus(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.SnapshotStatus{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ type TaskRepo struct {
|
|||
}
|
||||
|
||||
type ITaskRepo interface {
|
||||
Create(ctx context.Context, task *model.Task) error
|
||||
Save(ctx context.Context, task *model.Task) error
|
||||
GetFirst(opts ...DBOption) (model.Task, error)
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
||||
Update(ctx context.Context, task *model.Task) error
|
||||
|
|
@ -64,8 +65,8 @@ func (t TaskRepo) WithResourceID(id uint) DBOption {
|
|||
}
|
||||
}
|
||||
|
||||
func (t TaskRepo) Create(ctx context.Context, task *model.Task) error {
|
||||
return getTaskTx(ctx).Create(&task).Error
|
||||
func (t TaskRepo) Save(ctx context.Context, task *model.Task) error {
|
||||
return getTaskTx(ctx).Save(&task).Error
|
||||
}
|
||||
|
||||
func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
|||
if err := u.loadPath(&req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmd := exec.Command("docker compose", "-f", req.Path, "config")
|
||||
cmd := exec.Command("docker", "compose", "-f", req.Path, "config")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, errors.New(string(stdout))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
)
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface
|
|||
return 0, nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
|
||||
list, err := client.NetworkList(context.TODO(), network.ListOptions{})
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
|
@ -83,7 +82,7 @@ func (u *ContainerService) ListNetwork() ([]dto.Options, error) {
|
|||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
|
||||
list, err := client.NetworkList(context.TODO(), network.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, startTime time.Time, logPath string) error {
|
||||
func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, startTime time.Time) error {
|
||||
accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -206,15 +206,18 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, startTime time.Ti
|
|||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||
record.FileDir = "system_snapshot"
|
||||
|
||||
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
|
||||
req := dto.SnapshotCreate{
|
||||
Name: fmt.Sprintf("snapshot-1panel-%s-linux-%s-%s", versionItem.Value, loadOs(), startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)),
|
||||
Secret: cronjob.Secret,
|
||||
|
||||
SourceAccountIDs: record.SourceAccountIDs,
|
||||
DownloadAccountID: cronjob.DownloadAccountID,
|
||||
}
|
||||
name, err := NewISnapshotService().HandleSnapshot(true, logPath, req, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5), cronjob.Secret)
|
||||
if err != nil {
|
||||
if err := NewISnapshotService().HandleSnapshot(req); err != nil {
|
||||
return err
|
||||
}
|
||||
record.FileName = name + ".tar.gz"
|
||||
record.FileName = req.Name + ".tar.gz"
|
||||
|
||||
if err := backupRepo.CreateRecord(&record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
|
|
|
|||
|
|
@ -79,9 +79,8 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
|||
case "log":
|
||||
err = u.handleSystemLog(*cronjob, record.StartTime)
|
||||
case "snapshot":
|
||||
record.Records = u.generateLogsPath(*cronjob, record.StartTime)
|
||||
_ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records})
|
||||
err = u.handleSnapshot(*cronjob, record.StartTime, record.Records)
|
||||
err = u.handleSnapshot(*cronjob, record.StartTime)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,21 +2,21 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"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/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
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,18 +28,17 @@ type SnapshotService struct {
|
|||
|
||||
type ISnapshotService interface {
|
||||
SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
LoadSnapshotData() (dto.SnapshotData, error)
|
||||
SnapshotCreate(req dto.SnapshotCreate) error
|
||||
SnapshotReCreate(id uint) error
|
||||
SnapshotRecover(req dto.SnapshotRecover) error
|
||||
SnapshotRollback(req dto.SnapshotRecover) error
|
||||
SnapshotImport(req dto.SnapshotImport) error
|
||||
Delete(req dto.SnapshotBatchDelete) error
|
||||
|
||||
LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error)
|
||||
|
||||
UpdateDescription(req dto.UpdateDescription) error
|
||||
readFromJson(path string) (SnapshotJson, error)
|
||||
|
||||
HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error)
|
||||
HandleSnapshot(req dto.SnapshotCreate) error
|
||||
}
|
||||
|
||||
func NewISnapshotService() ISnapshotService {
|
||||
|
|
@ -70,8 +69,8 @@ func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
|
|||
}
|
||||
for _, snap := range req.Names {
|
||||
shortName := strings.TrimPrefix(snap, "snapshot_")
|
||||
nameItems := strings.Split(shortName, "_")
|
||||
if !strings.HasPrefix(shortName, "1panel_v") || !strings.HasSuffix(shortName, ".tar.gz") || len(nameItems) < 3 {
|
||||
nameItems := strings.Split(shortName, "-")
|
||||
if !strings.HasPrefix(shortName, "1panel-v") || !strings.HasSuffix(shortName, ".tar.gz") || len(nameItems) < 3 {
|
||||
return fmt.Errorf("incorrect snapshot name format of %s", shortName)
|
||||
}
|
||||
if strings.HasSuffix(snap, ".tar.gz") {
|
||||
|
|
@ -92,231 +91,47 @@ 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:]...)
|
||||
}
|
||||
if item.Label == "system_snapshot" {
|
||||
itemBackups[i].IsCheck = false
|
||||
for j := 0; j < len(item.Children); j++ {
|
||||
itemBackups[i].Children[j].IsCheck = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
||||
func (u *SnapshotService) LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error) {
|
||||
var data dto.SnapshotStatus
|
||||
status, err := snapshotRepo.GetStatus(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := copier.Copy(&data, &status); err != nil {
|
||||
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
|
||||
if _, err := u.HandleSnapshot(false, "", req, time.Now().Format(constant.DateTimeSlimLayout), req.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
|
||||
global.LOG.Info("start to recover panel by snapshot now")
|
||||
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasOs(snap.Name) && !strings.Contains(snap.Name, loadOs()) {
|
||||
return fmt.Errorf("restoring snapshots(%s) between different server architectures(%s) is not supported", snap.Name, loadOs())
|
||||
}
|
||||
if !req.IsNew && len(snap.InterruptStep) != 0 && len(snap.RollbackStatus) != 0 {
|
||||
return fmt.Errorf("the snapshot has been rolled back and cannot be restored again")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
|
||||
_ = settingRepo.Update("SystemStatus", "Recovering")
|
||||
go u.HandleSnapshotRecover(snap, true, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
|
||||
global.LOG.Info("start to rollback now")
|
||||
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.IsNew = false
|
||||
snap.InterruptStep = "Readjson"
|
||||
go u.HandleSnapshotRecover(snap, false, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) {
|
||||
var snap SnapshotJson
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return snap, fmt.Errorf("find snapshot json file in recover package failed, err: %v", err)
|
||||
}
|
||||
fileByte, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return snap, fmt.Errorf("read file from path %s failed, err: %v", path, err)
|
||||
}
|
||||
if err := json.Unmarshal(fileByte, &snap); err != nil {
|
||||
return snap, fmt.Errorf("unmarshal snapjson failed, err: %v", err)
|
||||
}
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error) {
|
||||
var (
|
||||
rootDir string
|
||||
snap model.Snapshot
|
||||
snapStatus model.SnapshotStatus
|
||||
err error
|
||||
)
|
||||
|
||||
if req.ID == 0 {
|
||||
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
|
||||
|
||||
name := fmt.Sprintf("1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
|
||||
if isCronjob {
|
||||
name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
|
||||
}
|
||||
rootDir = path.Join(global.CONF.System.Backup, "system", name)
|
||||
|
||||
snap = model.Snapshot{
|
||||
Name: name,
|
||||
Description: req.Description,
|
||||
SourceAccountIDs: req.SourceAccountIDs,
|
||||
DownloadAccountID: req.DownloadAccountID,
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusWaiting,
|
||||
}
|
||||
_ = snapshotRepo.Create(&snap)
|
||||
snapStatus.SnapID = snap.ID
|
||||
_ = snapshotRepo.CreateStatus(&snapStatus)
|
||||
} else {
|
||||
snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusWaiting})
|
||||
snapStatus, _ = snapshotRepo.GetStatus(snap.ID)
|
||||
if snapStatus.ID == 0 {
|
||||
snapStatus.SnapID = snap.ID
|
||||
_ = snapshotRepo.CreateStatus(&snapStatus)
|
||||
}
|
||||
rootDir = path.Join(global.CONF.System.Backup, fmt.Sprintf("system/%s", snap.Name))
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
itemHelper := snapHelper{SnapID: snap.ID, Status: &snapStatus, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
|
||||
backupPanelDir := path.Join(rootDir, "1panel")
|
||||
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
|
||||
backupDockerDir := path.Join(rootDir, "docker")
|
||||
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
|
||||
|
||||
jsonItem := SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: global.CONF.System.Backup,
|
||||
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
|
||||
}
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
if snapStatus.PanelInfo != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapJson(itemHelper, jsonItem, rootDir)
|
||||
}
|
||||
if snapStatus.Panel != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanel(itemHelper, backupPanelDir)
|
||||
}
|
||||
if snapStatus.DaemonJson != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapDaemonJson(itemHelper, backupDockerDir)
|
||||
}
|
||||
if snapStatus.AppData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapAppData(itemHelper, backupDockerDir)
|
||||
}
|
||||
if snapStatus.BackupData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapBackup(itemHelper, backupPanelDir)
|
||||
}
|
||||
|
||||
if !isCronjob {
|
||||
go func() {
|
||||
wg.Wait()
|
||||
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)
|
||||
}
|
||||
if snapStatus.Compress != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
return
|
||||
}
|
||||
if snapStatus.Upload != constant.StatusDone {
|
||||
snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir))
|
||||
}
|
||||
if snapStatus.Upload != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return "", nil
|
||||
}
|
||||
wg.Wait()
|
||||
if !checkIsAllDone(snap.ID) {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
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})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
return snap.Name, fmt.Errorf("snapshot %s compress failed", snap.Name)
|
||||
}
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir))
|
||||
if snapStatus.Upload != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
return snap.Name, fmt.Errorf("snapshot %s upload failed", snap.Name)
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
return snap.Name, nil
|
||||
BaseDir string `json:"baseDir"`
|
||||
BackupDataDir string `json:"backupDataDir"`
|
||||
Size uint64 `json:"size"`
|
||||
}
|
||||
|
||||
func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error {
|
||||
|
|
@ -333,7 +148,6 @@ func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error {
|
|||
}
|
||||
}
|
||||
|
||||
_ = snapshotRepo.DeleteStatus(snap.ID)
|
||||
if err := snapshotRepo.Delete(commonRepo.WithByID(snap.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -341,152 +155,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()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get all app installed for rebuild failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < len(appInstalls); i++ {
|
||||
wg.Add(1)
|
||||
appInstalls[i].Status = constant.Rebuilding
|
||||
_ = appInstallRepo.Save(context.Background(), &appInstalls[i])
|
||||
go func(app model.AppInstall) {
|
||||
defer wg.Done()
|
||||
dockerComposePath := app.GetComposePath()
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
out, err = compose.Up(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
app.Status = constant.Running
|
||||
_ = appInstallRepo.Save(context.Background(), &app)
|
||||
}(appInstalls[i])
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIsAllDone(snapID uint) bool {
|
||||
status, err := snapshotRepo.GetStatus(snapID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
isOK, _ := checkAllDone(status)
|
||||
return isOK
|
||||
}
|
||||
|
||||
func checkAllDone(status model.SnapshotStatus) (bool, string) {
|
||||
if status.Panel != constant.StatusDone {
|
||||
return false, status.Panel
|
||||
}
|
||||
if status.PanelInfo != constant.StatusDone {
|
||||
return false, status.PanelInfo
|
||||
}
|
||||
if status.DaemonJson != constant.StatusDone {
|
||||
return false, status.DaemonJson
|
||||
}
|
||||
if status.AppData != constant.StatusDone {
|
||||
return false, status.AppData
|
||||
}
|
||||
if status.BackupData != constant.StatusDone {
|
||||
return false, status.BackupData
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
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 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)
|
||||
logs += fmt.Sprintf("Snapshot size: %s \n", status.Size)
|
||||
logs += fmt.Sprintf("Upload snapshot file: %s \n", status.Upload)
|
||||
|
||||
file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
_, _ = file.Write([]byte(logs))
|
||||
}
|
||||
|
||||
func hasOs(name string) bool {
|
||||
return strings.Contains(name, "amd64") ||
|
||||
strings.Contains(name, "arm64") ||
|
||||
|
|
@ -544,3 +212,211 @@ 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
|
||||
}
|
||||
openrestyID := 0
|
||||
for _, app := range apps {
|
||||
if app.App.Key == constant.AppOpenresty {
|
||||
openrestyID = int(app.ID)
|
||||
}
|
||||
}
|
||||
websites, err := websiteRepo.List()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
appRelationMap := make(map[uint]uint)
|
||||
for _, website := range websites {
|
||||
if website.Type == constant.Deployment && website.AppInstallID != 0 {
|
||||
appRelationMap[uint(openrestyID)] = website.AppInstallID
|
||||
}
|
||||
}
|
||||
appRelations, err := appInstallResourceRepo.GetBy()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
for _, relation := range appRelations {
|
||||
appRelationMap[uint(relation.AppInstallId)] = relation.LinkId
|
||||
}
|
||||
appMap := make(map[uint]string)
|
||||
for _, app := range apps {
|
||||
appMap[app.ID] = fmt.Sprintf("%s-%s", app.App.Key, app.Name)
|
||||
}
|
||||
|
||||
appTreeMap := make(map[string]dto.DataTree)
|
||||
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}
|
||||
if app.App.Key == constant.AppOpenresty && len(websites) != 0 {
|
||||
itemAppData.IsDisable = true
|
||||
}
|
||||
if val, ok := appRelationMap[app.ID]; ok {
|
||||
itemAppData.RelationItemID = appMap[val]
|
||||
}
|
||||
sizeItem, err := fileOp.GetDirSize(appPath)
|
||||
if err == nil {
|
||||
itemAppData.Size = uint64(sizeItem)
|
||||
}
|
||||
itemApp.Size += itemAppData.Size
|
||||
data = append(data, itemApp)
|
||||
appTreeMap[fmt.Sprintf("%s-%s", itemApp.Key, itemApp.Name)] = itemAppData
|
||||
}
|
||||
|
||||
for key, val := range appTreeMap {
|
||||
if valRelation, ok := appTreeMap[val.RelationItemID]; ok {
|
||||
valRelation.IsDisable = true
|
||||
appTreeMap[val.RelationItemID] = valRelation
|
||||
|
||||
val.RelationItemID = valRelation.ID
|
||||
appTreeMap[key] = val
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(data); i++ {
|
||||
if val, ok := appTreeMap[fmt.Sprintf("%s-%s", data[i].Key, data[i].Name)]; ok {
|
||||
data[i].Children = append(data[i].Children, val)
|
||||
}
|
||||
}
|
||||
data = loadAppBackup(data, fileOp)
|
||||
data = loadAppImage(data)
|
||||
return data, nil
|
||||
}
|
||||
func loadAppBackup(list []dto.DataTree, fileOp fileUtils.FileOp) []dto.DataTree {
|
||||
for i := 0; i < len(list); i++ {
|
||||
appBackupPath := path.Join(global.CONF.System.BaseDir, "1panel/backup/app", list[i].Key, list[i].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)
|
||||
list[i].Size += itemAppBackup.Size
|
||||
}
|
||||
list[i].Children = append(list[i].Children, itemAppBackup)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
func loadAppImage(list []dto.DataTree) []dto.DataTree {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new docker client failed, err: %v", err)
|
||||
return list
|
||||
}
|
||||
defer client.Close()
|
||||
imageList, err := client.ImageList(context.Background(), image.ListOptions{})
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load image list failed, err: %v", err)
|
||||
return list
|
||||
}
|
||||
|
||||
for i := 0; i < len(list); i++ {
|
||||
itemAppImage := dto.DataTree{ID: uuid.NewString(), Label: "appImage"}
|
||||
stdout, err := cmd.Execf("cat %s | grep image: ", path.Join(global.CONF.System.BaseDir, "1panel/apps", list[i].Key, list[i].Name, "docker-compose.yml"))
|
||||
if err != nil {
|
||||
list[i].Children = append(list[i].Children, itemAppImage)
|
||||
continue
|
||||
}
|
||||
itemAppImage.Name = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(stdout), "\n", ""), "image: ", "")
|
||||
for _, imageItem := range imageList {
|
||||
for _, tag := range imageItem.RepoTags {
|
||||
if tag == itemAppImage.Name {
|
||||
itemAppImage.Size = uint64(imageItem.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
list[i].Children = append(list[i].Children, itemAppImage)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
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", "runtime", "docker", "secret", "task":
|
||||
itemData.IsDisable = true
|
||||
case "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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,145 +6,418 @@ 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/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
|
||||
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
|
||||
|
||||
req.Name = fmt.Sprintf("1panel-%s-linux-%s-%s", versionItem.Value, loadOs(), time.Now().Format(constant.DateTimeSlimLayout))
|
||||
appItem, _ := json.Marshal(req.AppData)
|
||||
panelItem, _ := json.Marshal(req.PanelData)
|
||||
backupItem, _ := json.Marshal(req.BackupData)
|
||||
snap := model.Snapshot{
|
||||
Name: req.Name,
|
||||
TaskID: req.TaskID,
|
||||
Secret: req.Secret,
|
||||
Description: req.Description,
|
||||
SourceAccountIDs: req.SourceAccountIDs,
|
||||
DownloadAccountID: req.DownloadAccountID,
|
||||
|
||||
AppData: string(appItem),
|
||||
PanelData: string(panelItem),
|
||||
BackupData: string(backupItem),
|
||||
WithMonitorData: req.WithMonitorData,
|
||||
WithLoginLog: req.WithLoginLog,
|
||||
WithOperationLog: req.WithOperationLog,
|
||||
WithTaskLog: req.WithTaskLog,
|
||||
WithSystemLog: req.WithSystemLog,
|
||||
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusWaiting,
|
||||
}
|
||||
if err := snapshotRepo.Create(&snap); err != nil {
|
||||
global.LOG.Errorf("create snapshot record to db failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
req.ID = snap.ID
|
||||
if err := u.HandleSnapshot(req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotReCreate(id uint) error {
|
||||
snap, err := snapshotRepo.Get(commonRepo.WithByID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskModel, err := taskRepo.GetFirst(taskRepo.WithResourceID(snap.ID), commonRepo.WithByType(task.TaskScopeSnapshot))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req dto.SnapshotCreate
|
||||
_ = copier.Copy(&req, snap)
|
||||
if err := json.Unmarshal([]byte(snap.PanelData), &req.PanelData); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(snap.AppData), &req.AppData); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(snap.BackupData), &req.BackupData); err != nil {
|
||||
return err
|
||||
}
|
||||
req.TaskID = taskModel.ID
|
||||
if err := u.HandleSnapshot(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) HandleSnapshot(req dto.SnapshotCreate) error {
|
||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeSnapshot, req.TaskID, req.ID)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new task for create snapshot failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
rootDir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", req.Name)
|
||||
itemHelper := snapHelper{SnapID: req.ID, Task: *taskItem, FileOp: files.NewFileOp(), Ctx: context.Background()}
|
||||
baseDir := path.Join(rootDir, "base")
|
||||
_ = os.MkdirAll(baseDir, os.ModePerm)
|
||||
|
||||
go func() {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapDBInfo",
|
||||
func(t *task.Task) error { return loadDbConn(&itemHelper, rootDir, req) },
|
||||
nil,
|
||||
)
|
||||
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapBaseInfo" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapBaseInfo",
|
||||
func(t *task.Task) error { return snapBaseData(itemHelper, baseDir) },
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapInstallApp" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapInstallApp",
|
||||
func(t *task.Task) error { return snapAppImage(itemHelper, req, rootDir) },
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapLocalBackup" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapLocalBackup",
|
||||
func(t *task.Task) error { return snapBackupData(itemHelper, req, rootDir) },
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapPanelData" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapPanelData",
|
||||
func(t *task.Task) error { return snapPanelData(itemHelper, req, rootDir) },
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
|
||||
taskItem.AddSubTask(
|
||||
"SnapCloseDBConn",
|
||||
func(t *task.Task) error {
|
||||
taskItem.Log("######################## 6 / 8 ########################")
|
||||
closeDatabase(itemHelper.snapAgentDB)
|
||||
closeDatabase(itemHelper.snapCoreDB)
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapCompress" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapCompress",
|
||||
func(t *task.Task) error { return snapCompress(itemHelper, rootDir, req.Secret) },
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"SnapUpload",
|
||||
func(t *task.Task) error {
|
||||
return snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
req.InterruptStep = ""
|
||||
}
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""})
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type snapHelper struct {
|
||||
SnapID uint
|
||||
Status *model.SnapshotStatus
|
||||
Ctx context.Context
|
||||
FileOp files.FileOp
|
||||
Wg *sync.WaitGroup
|
||||
SnapID uint
|
||||
snapAgentDB *gorm.DB
|
||||
snapCoreDB *gorm.DB
|
||||
Ctx context.Context
|
||||
FileOp files.FileOp
|
||||
Wg *sync.WaitGroup
|
||||
Task task.Task
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
snap.Status.PanelInfo = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status})
|
||||
}
|
||||
func loadDbConn(snap *snapHelper, targetDir string, req dto.SnapshotCreate) error {
|
||||
snap.Task.Log("######################## 1 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapDBInfo"))
|
||||
pathDB := path.Join(global.CONF.System.BaseDir, "1panel/db")
|
||||
|
||||
func snapPanel(snap snapHelper, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); 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 {
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.DaemonJson = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
|
||||
}
|
||||
|
||||
func snapAppData(snap snapHelper, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running})
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
err := snap.FileOp.CopyDir(pathDB, targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", pathDB), err)
|
||||
if err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
runtimes, err := runtimeRepo.List()
|
||||
|
||||
agentDb, err := newSnapDB(path.Join(targetDir, "db"), "agent.db")
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapNewDB", "agent"), err)
|
||||
if err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
imageRegex := regexp.MustCompile(`image:\s*(.*)`)
|
||||
var imageSaveList []string
|
||||
snap.snapAgentDB = agentDb
|
||||
coreDb, err := newSnapDB(path.Join(targetDir, "db"), "core.db")
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapNewDB", "core"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap.snapCoreDB = coreDb
|
||||
|
||||
if !req.WithMonitorData {
|
||||
err = os.Remove(path.Join(targetDir, "db/monitor.db"))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteMonitor"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !req.WithOperationLog {
|
||||
err = snap.snapCoreDB.Exec("DELETE FROM operation_logs").Error
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteOperationLog"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !req.WithLoginLog {
|
||||
err = snap.snapCoreDB.Exec("DELETE FROM login_logs").Error
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDeleteLoginLog"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = snap.snapAgentDB.Model(&model.Setting{}).Where("key = ?", "SystemIP").Updates(map[string]interface{}{"value": ""}).Error
|
||||
_ = snap.snapAgentDB.Where("id = ?", snap.SnapID).Delete(&model.Snapshot{}).Error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapBaseData(snap snapHelper, targetDir string) error {
|
||||
snap.Task.Log("######################## 2 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapBaseInfo"))
|
||||
|
||||
err := common.CopyFile("/usr/local/bin/1panel", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = common.CopyFile("/usr/local/bin/1panel_agent", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel_agent"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = common.CopyFile("/usr/local/bin/1pctl", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1pctl"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = common.CopyFile("/etc/systemd/system/1panel.service", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = common.CopyFile("/etc/systemd/system/1panel_agent.service", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel_agent.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if snap.FileOp.Stat("/etc/docker/daemon.json") {
|
||||
err = common.CopyFile("/etc/docker/daemon.json", targetDir)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/docker/daemon.json"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
remarkInfo, _ := json.MarshalIndent(SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: global.CONF.System.Backup,
|
||||
}, "", "\t")
|
||||
err = os.WriteFile(path.Join(targetDir, "snapshot.json"), remarkInfo, 0640)
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapCopy", path.Join(targetDir, "snapshot.json")), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapAppImage(snap snapHelper, req dto.SnapshotCreate, targetDir string) error {
|
||||
snap.Task.Log("######################## 3 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapInstallApp"))
|
||||
|
||||
var imageList []string
|
||||
existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG")
|
||||
existImages := strings.Split(existStr, "\n")
|
||||
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(imageList) != 0 {
|
||||
snap.Task.Logf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz"))
|
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz"))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapDockerSave"), errors.New(std))
|
||||
if err != nil {
|
||||
snap.Task.LogFailedWithErr(i18n.GetMsgByKey("SnapDockerSave"), errors.New(std))
|
||||
return errors.New(std)
|
||||
}
|
||||
snap.Task.LogSuccess(i18n.GetMsgByKey("SnapDockerSave"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapBackupData(snap snapHelper, req dto.SnapshotCreate, targetDir string) error {
|
||||
snap.Task.Log("######################## 4 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapLocalBackup"))
|
||||
|
||||
excludes := loadBackupExcludes(snap, req.BackupData)
|
||||
for _, item := range req.AppData {
|
||||
for _, itemApp := range item.Children {
|
||||
if itemApp.Label == "appBackup" {
|
||||
excludes = append(excludes, loadAppBackupExcludes([]dto.DataTree{itemApp})...)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := snap.FileOp.TarGzCompressPro(false, global.CONF.System.Backup, path.Join(targetDir, "1panel_backup.tar.gz"), "", strings.Join(excludes, ";"))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressBackup"), err)
|
||||
|
||||
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 err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std})
|
||||
return
|
||||
return err
|
||||
}
|
||||
func loadBackupExcludes(snap snapHelper, req []dto.DataTree) []string {
|
||||
var excludes []string
|
||||
for _, item := range req {
|
||||
if len(item.Children) == 0 {
|
||||
if item.IsCheck {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(item.Path, path.Join(global.CONF.System.Backup, "system_snapshot")) {
|
||||
fmt.Println(strings.TrimSuffix(item.Name, ".tar.gz"))
|
||||
if err := snap.snapAgentDB.Debug().Where("name = ? AND download_account_id = ?", strings.TrimSuffix(item.Name, ".tar.gz"), "1").Delete(&model.Snapshot{}).Error; err != nil {
|
||||
snap.Task.LogWithStatus("delete snapshot from database", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path))
|
||||
if err := snap.snapAgentDB.Debug().Where("file_dir = ? AND file_name = ?", strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path)).Delete(&model.BackupRecord{}).Error; err != nil {
|
||||
snap.Task.LogWithStatus("delete backup file from database", err)
|
||||
}
|
||||
}
|
||||
excludes = append(excludes, "."+strings.TrimPrefix(item.Path, global.CONF.System.Backup))
|
||||
} else {
|
||||
excludes = append(excludes, loadBackupExcludes(snap, item.Children)...)
|
||||
}
|
||||
}
|
||||
snap.Status.AppData = constant.StatusDone
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone})
|
||||
return excludes
|
||||
}
|
||||
|
||||
func snapBackup(snap snapHelper, 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 {
|
||||
status = err.Error()
|
||||
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)...)
|
||||
}
|
||||
}
|
||||
snap.Status.BackupData = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status})
|
||||
return excludes
|
||||
}
|
||||
|
||||
func snapPanelData(snap snapHelper, targetDir string) {
|
||||
_ = 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, "") + ";")
|
||||
func snapPanelData(snap snapHelper, req dto.SnapshotCreate, targetDir string) error {
|
||||
snap.Task.Log("######################## 5 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapPanelData"))
|
||||
|
||||
excludes := loadPanelExcludes(req.PanelData)
|
||||
for _, item := range req.AppData {
|
||||
for _, itemApp := range item.Children {
|
||||
if itemApp.Label == "appData" {
|
||||
excludes = append(excludes, loadPanelExcludes([]dto.DataTree{itemApp})...)
|
||||
}
|
||||
}
|
||||
}
|
||||
excludes = append(excludes, "./tmp")
|
||||
excludes = append(excludes, "./cache")
|
||||
excludes = append(excludes, "./uploads")
|
||||
excludes = append(excludes, "./db")
|
||||
excludes = append(excludes, "./resource")
|
||||
if !req.WithSystemLog {
|
||||
excludes = append(excludes, "./log/1Panel*")
|
||||
}
|
||||
if !req.WithTaskLog {
|
||||
excludes = append(excludes, "./log/App")
|
||||
excludes = append(excludes, "./log/Snapshot")
|
||||
excludes = append(excludes, "./log/AppStore")
|
||||
excludes = append(excludes, "./log/Website")
|
||||
}
|
||||
|
||||
rootDir := path.Join(global.CONF.System.BaseDir, "1panel")
|
||||
if strings.Contains(global.CONF.System.Backup, rootDir) {
|
||||
excludes = append(excludes, "."+strings.ReplaceAll(global.CONF.System.Backup, rootDir, ""))
|
||||
}
|
||||
ignoreVal, _ := settingRepo.Get(settingRepo.WithByKey("SnapshotIgnore"))
|
||||
rules := strings.Split(ignoreVal.Value, ",")
|
||||
|
|
@ -152,129 +425,93 @@ 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 {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting})
|
||||
err := snap.FileOp.TarGzCompressPro(false, rootDir, path.Join(targetDir, "1panel_data.tar.gz"), "", strings.Join(excludes, ";"))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressPanel"), err)
|
||||
|
||||
snap.Status.PanelData = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status})
|
||||
_ = settingRepo.Update("SystemIP", sysIP.Value)
|
||||
return err
|
||||
}
|
||||
func loadPanelExcludes(req []dto.DataTree) []string {
|
||||
var excludes []string
|
||||
for _, item := range req {
|
||||
if len(item.Children) == 0 {
|
||||
if !item.IsCheck {
|
||||
excludes = append(excludes, "."+strings.TrimPrefix(item.Path, path.Join(global.CONF.System.BaseDir, "1panel")))
|
||||
}
|
||||
} else {
|
||||
excludes = append(excludes, loadPanelExcludes(item.Children)...)
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
}
|
||||
|
||||
func snapCompress(snap snapHelper, rootDir string, secret string) {
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusRunning})
|
||||
func snapCompress(snap snapHelper, rootDir string, secret string) error {
|
||||
snap.Task.Log("######################## 7 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapCompress"))
|
||||
|
||||
tmpDir := path.Join(global.CONF.System.TmpDir, "system")
|
||||
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir))
|
||||
if err := handleSnapTar(rootDir, tmpDir, fileName, "", secret); err != nil {
|
||||
snap.Status.Compress = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()})
|
||||
return
|
||||
err := snap.FileOp.TarGzCompressPro(true, rootDir, path.Join(tmpDir, fileName), secret, "")
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCompressFile"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(path.Join(tmpDir, fileName))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapCheckCompress"), err)
|
||||
if err != nil {
|
||||
snap.Status.Compress = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
size := common.LoadSizeUnit2F(float64(stat.Size()))
|
||||
global.LOG.Debugf("compress successful! size of file: %s", size)
|
||||
snap.Status.Compress = constant.StatusDone
|
||||
snap.Status.Size = size
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusDone, "size": size})
|
||||
|
||||
global.LOG.Debugf("remove snapshot file %s", rootDir)
|
||||
snap.Task.Logf(i18n.GetWithName("SnapCompressSize", size))
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}
|
||||
|
||||
func snapUpload(snap snapHelper, accounts string, file string) {
|
||||
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file))
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusUploading})
|
||||
accountMap, err := NewBackupClientMap(strings.Split(accounts, ","))
|
||||
if err != nil {
|
||||
snap.Status.Upload = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
|
||||
return
|
||||
}
|
||||
targetAccounts := strings.Split(accounts, ",")
|
||||
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)
|
||||
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)
|
||||
}
|
||||
snap.Status.Upload = constant.StatusDone
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone})
|
||||
|
||||
global.LOG.Debugf("remove snapshot file %s", source)
|
||||
_ = 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)
|
||||
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
|
||||
}
|
||||
|
||||
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 snapUpload(snap snapHelper, accounts string, file string) error {
|
||||
snap.Task.Log("######################## 8 / 8 ########################")
|
||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload"))
|
||||
|
||||
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file))
|
||||
accountMap, err := NewBackupClientMap(strings.Split(accounts, ","))
|
||||
snap.Task.LogWithStatus(i18n.GetMsgByKey("SnapLoadBackup"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetAccounts := strings.Split(accounts, ",")
|
||||
for _, item := range targetAccounts {
|
||||
snap.Task.LogStart(i18n.GetWithName("SnapUploadTo", fmt.Sprintf("[%s] %s", accountMap[item].name, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file)))))
|
||||
_, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file)))
|
||||
snap.Task.LogWithStatus(i18n.GetWithName("SnapUploadRes", accountMap[item].name), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = os.Remove(source)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSnapDB(dir, file string) (*gorm.DB, error) {
|
||||
db, _ := gorm.Open(sqlite.Open(path.Join(dir, file)), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB.SetConnMaxIdleTime(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func closeDatabase(db *gorm.DB) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
|
@ -10,219 +11,361 @@ import (
|
|||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover bool, 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))
|
||||
return
|
||||
}
|
||||
}
|
||||
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", snapFileDir))
|
||||
if err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "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 err := recoverAppData(snapFileDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "DockerDir", 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!")
|
||||
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())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel binary 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())
|
||||
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!")
|
||||
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())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel backups from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel data from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
_ = rebuildAllAppInstall()
|
||||
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))
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
|
||||
type snapRecoverHelper struct {
|
||||
FileOp files.FileOp
|
||||
Task *task.Task
|
||||
}
|
||||
|
||||
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)
|
||||
func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
|
||||
global.LOG.Info("start to recover panel by snapshot now")
|
||||
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasOs(snap.Name) && !strings.Contains(snap.Name, loadOs()) {
|
||||
errInfo := fmt.Sprintf("restoring snapshots(%s) between different server architectures(%s) is not supported", snap.Name, loadOs())
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusFailed, "recover_message": errInfo})
|
||||
return errors.New(errInfo)
|
||||
}
|
||||
if len(snap.RollbackStatus) != 0 && snap.RollbackStatus != constant.StatusSuccess {
|
||||
req.IsNew = true
|
||||
}
|
||||
if !req.IsNew && (snap.InterruptStep == "RecoverDownload" || snap.InterruptStep == "RecoverDecompress" || snap.InterruptStep == "BackupBeforeRecover") {
|
||||
req.IsNew = true
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
|
||||
_ = settingRepo.Update("SystemStatus", "Recovering")
|
||||
|
||||
if len(snap.InterruptStep) == 0 {
|
||||
req.IsNew = true
|
||||
}
|
||||
if len(snap.TaskRecoverID) != 0 {
|
||||
req.TaskID = snap.TaskRecoverID
|
||||
} else {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"task_recover_id": req.TaskID})
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(snap.Name, task.TaskRecover, task.TaskScopeSnapshot, req.TaskID, snap.ID)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new task for create snapshot failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
rootDir := path.Join(global.CONF.System.TmpDir, "system", snap.Name)
|
||||
if _, err := os.Stat(rootDir); err != nil && os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(rootDir, os.ModePerm)
|
||||
}
|
||||
itemHelper := snapRecoverHelper{Task: taskItem, FileOp: files.NewFileOp()}
|
||||
|
||||
go func() {
|
||||
_ = global.Cron.Stop()
|
||||
defer func() {
|
||||
global.Cron.Start()
|
||||
}()
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "RecoverDownload" || req.ReDownload {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverDownload",
|
||||
func(t *task.Task) error { return handleDownloadSnapshot(&itemHelper, snap, rootDir) },
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "RecoverDecompress" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverDecompress",
|
||||
func(t *task.Task) error {
|
||||
itemHelper.Task.Log("######################## 2 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetWithName("RecoverDecompress", snap.Name))
|
||||
err := itemHelper.FileOp.TarGzExtractPro(fmt.Sprintf("%s/%s.tar.gz", rootDir, snap.Name), rootDir, req.Secret)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Decompress"), err)
|
||||
return err
|
||||
},
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "BackupBeforeRecover" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"BackupBeforeRecover",
|
||||
func(t *task.Task) error { return backupBeforeRecover(snap.Name, &itemHelper) },
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
|
||||
var snapJson SnapshotJson
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"Readjson",
|
||||
func(t *task.Task) error {
|
||||
snapJson, err = readFromJson(path.Join(rootDir, snap.Name), &itemHelper)
|
||||
return err
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if req.IsNew || snap.InterruptStep == "RecoverApp" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverApp",
|
||||
func(t *task.Task) error { return recoverAppData(path.Join(rootDir, snap.Name), &itemHelper) },
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "RecoverBaseData" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverBaseData",
|
||||
func(t *task.Task) error { return recoverBaseData(path.Join(rootDir, snap.Name, "base"), &itemHelper) },
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "RecoverDBData" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverDBData",
|
||||
func(t *task.Task) error { return recoverDBData(path.Join(rootDir, snap.Name, "db"), &itemHelper) },
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "RecoverBackups" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverBackups",
|
||||
func(t *task.Task) error {
|
||||
itemHelper.Task.Log("######################## 8 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetWithName("RecoverBackups", snap.Name))
|
||||
err := itemHelper.FileOp.TarGzExtractPro(path.Join(rootDir, snap.Name, "/1panel_backup.tar.gz"), snapJson.BackupDataDir, "")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Decompress"), err)
|
||||
return err
|
||||
},
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "RecoverPanelData" {
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverPanelData",
|
||||
func(t *task.Task) error {
|
||||
itemHelper.Task.Log("######################## 9 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetWithName("RecoverPanelData", snap.Name))
|
||||
err := itemHelper.FileOp.TarGzExtractPro(path.Join(rootDir, snap.Name, "/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), "")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Decompress"), err)
|
||||
return err
|
||||
},
|
||||
nil,
|
||||
)
|
||||
req.IsNew = true
|
||||
}
|
||||
taskItem.AddSubTaskWithAlias(
|
||||
"RecoverDBData",
|
||||
func(t *task.Task) error {
|
||||
return restartCompose(path.Join(snapJson.BaseDir, "1panel/docker/compose"), &itemHelper)
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"recover_status": constant.StatusFailed, "recover_message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep})
|
||||
return
|
||||
}
|
||||
_ = os.RemoveAll(rootDir)
|
||||
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleDownloadSnapshot(itemHelper *snapRecoverHelper, snap model.Snapshot, targetDir string) error {
|
||||
itemHelper.Task.Log("######################## 1 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDownload"))
|
||||
|
||||
account, client, err := NewBackupClientWithID(snap.DownloadAccountID)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("RecoverDownloadAccount", fmt.Sprintf("%s - %s", account.Type, account.Name)), err)
|
||||
pathItem := account.BackupPath
|
||||
if account.BackupPath != "/" {
|
||||
pathItem = strings.TrimPrefix(account.BackupPath, "/")
|
||||
}
|
||||
filePath := fmt.Sprintf("%s/%s.tar.gz", targetDir, snap.Name)
|
||||
_ = os.RemoveAll(filePath)
|
||||
ok, err := client.Download(path.Join(pathItem, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name)), filePath)
|
||||
if err != nil || !ok {
|
||||
return fmt.Errorf("download file %s from %s failed, err: %v", snap.Name, account.Name, err)
|
||||
_, err = client.Download(path.Join(pathItem, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name)), filePath)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Download"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
func backupBeforeRecover(name string, itemHelper *snapRecoverHelper) error {
|
||||
itemHelper.Task.Log("######################## 3 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("BackupBeforeRecover"))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
err := itemHelper.FileOp.CopyDirWithExclude(path.Join(global.CONF.System.BaseDir, "1panel"), rootDir, []string{"cache", "tmp"})
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", path.Join(global.CONF.System.BaseDir, "1panel")), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyDirWithExclude(global.CONF.System.Backup, rootDir, []string{"system_snapshot"})
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", global.CONF.System.Backup), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/usr/local/bin/1pctl", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1pctl"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/usr/local/bin/1panel", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/usr/local/bin/1panel_agent", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel_agent"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/etc/systemd/system/1panel.service", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/etc/systemd/system/1panel_agent.service", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel_agent.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile("/etc/docker/daemon.json", baseDir)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/docker/daemon.json"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverAppData(src string) error {
|
||||
if _, err := os.Stat(path.Join(src, "docker/docker_image.tar")); 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"))
|
||||
func readFromJson(rootDir string, itemHelper *snapRecoverHelper) (SnapshotJson, error) {
|
||||
itemHelper.Task.Log("######################## 4 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("Readjson"))
|
||||
|
||||
snapJsonPath := path.Join(rootDir, "base/snapshot.json")
|
||||
var snap SnapshotJson
|
||||
_, err := os.Stat(snapJsonPath)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("ReadjsonPath"), err)
|
||||
if err != nil {
|
||||
return errors.New(std)
|
||||
return snap, err
|
||||
}
|
||||
return err
|
||||
fileByte, err := os.ReadFile(snapJsonPath)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("ReadjsonContent"), err)
|
||||
if err != nil {
|
||||
return snap, err
|
||||
}
|
||||
err = json.Unmarshal(fileByte, &snap)
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("ReadjsonMarshal"), err)
|
||||
if err != nil {
|
||||
return snap, err
|
||||
}
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func recoverDaemonJson(src string, fileOp files.FileOp) error {
|
||||
func recoverAppData(src string, itemHelper *snapRecoverHelper) error {
|
||||
itemHelper.Task.Log("######################## 5 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverApp"))
|
||||
|
||||
if _, err := os.Stat(path.Join(src, "images.tar.gz")); err != nil {
|
||||
itemHelper.Task.Log(i18n.GetMsgByKey("RecoverAppEmpty"))
|
||||
return nil
|
||||
} else {
|
||||
std, err := cmd.Execf("docker load < %s", path.Join(src, "images.tar.gz"))
|
||||
if err != nil {
|
||||
itemHelper.Task.LogFailedWithErr(i18n.GetMsgByKey("RecoverAppImage"), errors.New(std))
|
||||
return fmt.Errorf("docker load images failed, err: %v", err)
|
||||
}
|
||||
itemHelper.Task.LogSuccess(i18n.GetMsgByKey("RecoverAppImage"))
|
||||
}
|
||||
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverAppList"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < len(appInstalls); i++ {
|
||||
wg.Add(1)
|
||||
appInstalls[i].Status = constant.Rebuilding
|
||||
_ = appInstallRepo.Save(context.Background(), &appInstalls[i])
|
||||
go func(app model.AppInstall) {
|
||||
defer wg.Done()
|
||||
dockerComposePath := app.GetComposePath()
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
out, err = compose.Up(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
app.Status = constant.Running
|
||||
_ = appInstallRepo.Save(context.Background(), &app)
|
||||
}(appInstalls[i])
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverBaseData(src string, itemHelper *snapRecoverHelper) error {
|
||||
itemHelper.Task.Log("######################## 6 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("SnapBaseInfo"))
|
||||
|
||||
err := itemHelper.FileOp.CopyFile(path.Join(src, "1pctl"), "/usr/local/bin")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1pctl"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = itemHelper.FileOp.CopyFile(path.Join(src, "1panel"), "/usr/local/bin")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile(path.Join(src, "1panel_agent"), "/usr/local/bin")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel_agent"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile(path.Join(src, "1panel.service"), "/etc/systemd/system")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = itemHelper.FileOp.CopyFile(path.Join(src, "1panel_agent.service"), "/etc/systemd/system")
|
||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel_agent.service"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
daemonJsonPath := "/etc/docker/daemon.json"
|
||||
_, errSrc := os.Stat(path.Join(src, "docker/daemon.json"))
|
||||
_, errPath := os.Stat(daemonJsonPath)
|
||||
if os.IsNotExist(errSrc) && os.IsNotExist(errPath) {
|
||||
global.LOG.Debug("the daemon.json file does not exist, nothing happens.")
|
||||
itemHelper.Task.Log(i18n.GetMsgByKey("RecoverDaemonJsonEmpty"))
|
||||
return nil
|
||||
}
|
||||
if errSrc == nil {
|
||||
if err := fileOp.CopyFile(path.Join(src, "docker/daemon.json"), "/etc/docker"); err != nil {
|
||||
err = itemHelper.FileOp.CopyFile(path.Join(src, "docker/daemon.json"), "/etc/docker")
|
||||
itemHelper.Task.Log(i18n.GetMsgByKey("RecoverDaemonJson"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("recover docker daemon.json failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -231,21 +374,25 @@ 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, itemHelper *snapRecoverHelper) error {
|
||||
itemHelper.Task.Log("######################## 7 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData"))
|
||||
err := itemHelper.FileOp.CopyDirWithExclude(src, path.Join(global.CONF.System.BaseDir, "1panel"), nil)
|
||||
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
func restartCompose(composePath string) {
|
||||
func restartCompose(composePath string, itemHelper *snapRecoverHelper) error {
|
||||
itemHelper.Task.Log("######################## 10 / 10 ########################")
|
||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverCompose"))
|
||||
|
||||
composes, err := composeRepo.ListRecord()
|
||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverComposeList"), err)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
for _, compose := range composes {
|
||||
pathItem := path.Join(composePath, compose.Name, "docker-compose.yml")
|
||||
if _, err := os.Stat(pathItem); err != nil {
|
||||
|
|
@ -254,8 +401,10 @@ func restartCompose(composePath string) {
|
|||
upCmd := fmt.Sprintf("docker compose -f %s up -d", pathItem)
|
||||
stdout, err := cmd.Exec(upCmd)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("%s failed, err: %v", upCmd, stdout)
|
||||
itemHelper.Task.LogFailedWithErr(i18n.GetMsgByKey("RecoverCompose"), errors.New(stdout))
|
||||
continue
|
||||
}
|
||||
itemHelper.Task.LogSuccess(i18n.GetWithName("RecoverComposeItem", pathItem))
|
||||
}
|
||||
global.LOG.Debug("restart all compose successful!")
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
107
agent/app/service/snapshot_rollback.go
Normal file
107
agent/app/service/snapshot_rollback.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
|
||||
global.LOG.Info("start to rollback now")
|
||||
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(snap.TaskRollbackID) != 0 {
|
||||
req.TaskID = snap.TaskRollbackID
|
||||
} else {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"task_rollback_id": req.TaskID})
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(snap.Name, task.TaskRollback, task.TaskScopeSnapshot, req.TaskID, snap.ID)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new task for create snapshot failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
rootDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
|
||||
baseDir := path.Join(rootDir, "base")
|
||||
|
||||
FileOp := files.NewFileOp()
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/usr/local/bin/1pctl"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "1pctl"), "/usr/local/bin")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "1panel"), "/usr/local/bin")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/usr/local/bin/1panel_agent"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "1panel_agent"), "/usr/local/bin")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel.service"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "1panel.service"), "/etc/systemd/system")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/etc/systemd/system/1panel_agent.service"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "1panel.service"), "/etc/systemd/system")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", "/etc/docker/daemon.json"),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyFile(path.Join(baseDir, "daemon.json"), "/etc/docker")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", global.CONF.System.Backup),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyDir(path.Join(rootDir, "backup"), global.CONF.System.Backup)
|
||||
},
|
||||
nil,
|
||||
)
|
||||
taskItem.AddSubTask(
|
||||
i18n.GetWithName("SnapCopy", global.CONF.System.BaseDir),
|
||||
func(t *task.Task) error {
|
||||
return FileOp.CopyDir(path.Join(rootDir, "1panel"), global.CONF.System.BaseDir)
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"rollback_status": constant.StatusFailed, "rollback_message": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{
|
||||
"recover_status": "",
|
||||
"recover_message": "",
|
||||
"rollback_status": "",
|
||||
"rollback_message": "",
|
||||
"interrupt_step": "",
|
||||
})
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3,16 +3,17 @@ package task
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/google/uuid"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ActionFunc func(*Task) error
|
||||
|
|
@ -33,6 +34,7 @@ type Task struct {
|
|||
type SubTask struct {
|
||||
RootTask *Task
|
||||
Name string
|
||||
StepAlias string
|
||||
Retry int
|
||||
Timeout time.Duration
|
||||
Action ActionFunc
|
||||
|
|
@ -50,6 +52,8 @@ const (
|
|||
TaskUpdate = "TaskUpdate"
|
||||
TaskRestart = "TaskRestart"
|
||||
TaskBackup = "TaskBackup"
|
||||
TaskRecover = "TaskRecover"
|
||||
TaskRollback = "TaskRollback"
|
||||
TaskSync = "TaskSync"
|
||||
TaskBuild = "TaskBuild"
|
||||
)
|
||||
|
|
@ -60,6 +64,7 @@ const (
|
|||
TaskScopeRuntime = "Runtime"
|
||||
TaskScopeDatabase = "Database"
|
||||
TaskScopeAppStore = "AppStore"
|
||||
TaskScopeSnapshot = "Snapshot"
|
||||
TaskScopeRuntimeExtension = "RuntimeExtension"
|
||||
)
|
||||
|
||||
|
|
@ -111,6 +116,11 @@ func (t *Task) AddSubTask(name string, action ActionFunc, rollback RollbackFunc)
|
|||
t.SubTasks = append(t.SubTasks, subTask)
|
||||
}
|
||||
|
||||
func (t *Task) AddSubTaskWithAlias(key string, action ActionFunc, rollback RollbackFunc) {
|
||||
subTask := &SubTask{RootTask: t, Name: i18n.GetMsgByKey(key), StepAlias: key, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: rollback}
|
||||
t.SubTasks = append(t.SubTasks, subTask)
|
||||
}
|
||||
|
||||
func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) {
|
||||
subTask := &SubTask{RootTask: t, Name: name, Retry: retry, Timeout: timeout, Action: action, Rollback: rollback}
|
||||
t.SubTasks = append(t.SubTasks, subTask)
|
||||
|
|
@ -166,13 +176,13 @@ func (t *Task) updateTask(task *model.Task) {
|
|||
}
|
||||
|
||||
func (t *Task) Execute() error {
|
||||
if err := t.taskRepo.Create(context.Background(), t.Task); err != nil {
|
||||
if err := t.taskRepo.Save(context.Background(), t.Task); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
t.Log(i18n.GetWithName("TaskStart", t.Name))
|
||||
for _, subTask := range t.SubTasks {
|
||||
t.Task.CurrentStep = subTask.Name
|
||||
t.Task.CurrentStep = subTask.StepAlias
|
||||
t.updateTask(t.Task)
|
||||
if err = subTask.Execute(); err == nil {
|
||||
if subTask.Rollback != nil {
|
||||
|
|
@ -221,6 +231,10 @@ func (t *Task) Log(msg string) {
|
|||
t.Logger.Printf(msg)
|
||||
}
|
||||
|
||||
func (t *Task) Logf(format string, v ...any) {
|
||||
t.Logger.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (t *Task) LogFailed(msg string) {
|
||||
t.Logger.Printf(msg + i18n.GetMsgByKey("Failed"))
|
||||
}
|
||||
|
|
@ -232,6 +246,9 @@ func (t *Task) LogFailedWithErr(msg string, err error) {
|
|||
func (t *Task) LogSuccess(msg string) {
|
||||
t.Logger.Printf(msg + i18n.GetMsgByKey("Success"))
|
||||
}
|
||||
func (t *Task) LogSuccessf(format string, v ...any) {
|
||||
t.Logger.Printf(fmt.Sprintf(format, v...) + i18n.GetMsgByKey("Success"))
|
||||
}
|
||||
|
||||
func (t *Task) LogStart(msg string) {
|
||||
t.Logger.Printf(fmt.Sprintf("%s%s", i18n.GetMsgByKey("Start"), msg))
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ TaskDelete: "Delete"
|
|||
TaskUpgrade: "Upgrade"
|
||||
TaskUpdate: "Update"
|
||||
TaskRestart: "Restart"
|
||||
TaskRollback: "Rollback"
|
||||
Website: "Website"
|
||||
App: "App"
|
||||
Runtime: "Runtime"
|
||||
|
|
@ -247,3 +248,28 @@ SubTask: "Subtask"
|
|||
RuntimeExtension: "Runtime Extension"
|
||||
TaskBuild: "Build"
|
||||
|
||||
# task - snapshot
|
||||
Snapshot: "Snapshot"
|
||||
SnapDBInfo: "Write 1Panel database information"
|
||||
SnapCopy: "Copy files & directories {{ .name }} "
|
||||
SnapNewDB: "Initialize database {{ .name }} connection "
|
||||
SnapDeleteOperationLog: "Delete operation log"
|
||||
SnapDeleteLoginLog: "Delete access log"
|
||||
SnapDeleteMonitor: "Delete monitoring data"
|
||||
SnapRemoveSystemIP: "Remove system IP"
|
||||
SnapBaseInfo: "Write 1Panel basic information"
|
||||
SnapInstallApp: "Backup installed applications in 1Panel"
|
||||
SnapDockerSave: "Compress installed applications"
|
||||
SnapLocalBackup: "Backup 1Panel local backup directory"
|
||||
SnapCompressBackup: "Compress local backup directory"
|
||||
SnapPanelData: "Backup 1Panel data directory"
|
||||
SnapCompressPanel: "Compress data directory"
|
||||
SnapCloseDBConn: "Close database connection"
|
||||
SnapCompress: "Create snapshot file"
|
||||
SnapCompressFile: "Compress snapshot file"
|
||||
SnapCheckCompress: "Check snapshot compressed file"
|
||||
SnapCompressSize: "Snapshot file size {{ .name }}"
|
||||
SnapUpload: "Upload snapshot file"
|
||||
SnapLoadBackup: "Get backup account information"
|
||||
SnapUploadTo: "Upload snapshot file to {{ .name }}"
|
||||
SnapUploadRes: "Upload snapshot file to {{ .name }}"
|
||||
|
|
@ -221,6 +221,7 @@ TaskDelete: "刪除"
|
|||
TaskUpgrade: "升級"
|
||||
TaskUpdate: "更新"
|
||||
TaskRestart: "重啟"
|
||||
TaskRollback: "回滚"
|
||||
Website: "網站"
|
||||
App: "應用"
|
||||
Runtime: "運行環境"
|
||||
|
|
@ -250,3 +251,28 @@ RuntimeExtension: "運行環境擴展"
|
|||
TaskBuild: "構建"
|
||||
|
||||
|
||||
# task - snapshot
|
||||
Snapshot: "快照"
|
||||
SnapDBInfo: "寫入 1Panel 資料庫資訊"
|
||||
SnapCopy: "複製檔案&目錄 {{ .name }} "
|
||||
SnapNewDB: "初始化資料庫 {{ .name }} 連接 "
|
||||
SnapDeleteOperationLog: "刪除操作日誌"
|
||||
SnapDeleteLoginLog: "刪除訪問日誌"
|
||||
SnapDeleteMonitor: "刪除監控數據"
|
||||
SnapRemoveSystemIP: "移除系統 IP"
|
||||
SnapBaseInfo: "寫入 1Panel 基本資訊"
|
||||
SnapInstallApp: "備份 1Panel 已安裝應用"
|
||||
SnapDockerSave: "壓縮已安裝應用"
|
||||
SnapLocalBackup: "備份 1Panel 本地備份目錄"
|
||||
SnapCompressBackup: "壓縮本地備份目錄"
|
||||
SnapPanelData: "備份 1Panel 資料目錄"
|
||||
SnapCompressPanel: "壓縮資料目錄"
|
||||
SnapCloseDBConn: "關閉資料庫連接"
|
||||
SnapCompress: "製作快照檔案"
|
||||
SnapCompressFile: "壓縮快照檔案"
|
||||
SnapCheckCompress: "檢查快照壓縮檔案"
|
||||
SnapCompressSize: "快照檔案大小 {{ .name }}"
|
||||
SnapUpload: "上傳快照檔案"
|
||||
SnapLoadBackup: "獲取備份帳號資訊"
|
||||
SnapUploadTo: "上傳快照檔案到 {{ .name }}"
|
||||
SnapUploadRes: "上傳快照檔案到 {{ .name }}"
|
||||
|
|
@ -223,6 +223,8 @@ TaskUpgrade: "升级"
|
|||
TaskUpdate: "更新"
|
||||
TaskRestart: "重启"
|
||||
TaskBackup: "备份"
|
||||
TaskRecover: "恢复"
|
||||
TaskRollback: "回滚"
|
||||
Website: "网站"
|
||||
App: "应用"
|
||||
Runtime: "运行环境"
|
||||
|
|
@ -249,4 +251,55 @@ TaskSync: "同步"
|
|||
LocalApp: "本地应用"
|
||||
SubTask: "子任务"
|
||||
RuntimeExtension: "运行环境扩展"
|
||||
TaskBuild: "构建"
|
||||
|
||||
# task - snapshot
|
||||
Snapshot: "快照"
|
||||
SnapDBInfo: "写入 1Panel 数据库信息"
|
||||
SnapCopy: "复制文件&目录 {{ .name }} "
|
||||
SnapNewDB: "初始化数据库 {{ .name }} 连接 "
|
||||
SnapDeleteOperationLog: "删除操作日志"
|
||||
SnapDeleteLoginLog: "删除访问日志"
|
||||
SnapDeleteMonitor: "删除监控数据"
|
||||
SnapRemoveSystemIP: "移除系统 IP"
|
||||
SnapBaseInfo: "写入 1Panel 基本信息"
|
||||
SnapInstallApp: "备份 1Panel 已安装应用"
|
||||
SnapDockerSave: "压缩已安装应用"
|
||||
SnapLocalBackup: "备份 1Panel 本地备份目录"
|
||||
SnapCompressBackup: "压缩本地备份目录"
|
||||
SnapPanelData: "备份 1Panel 数据目录"
|
||||
SnapCompressPanel: "压缩数据目录"
|
||||
SnapCloseDBConn: "关闭数据库连接"
|
||||
SnapCompress: "制作快照文件"
|
||||
SnapCompressFile: "压缩快照文件"
|
||||
SnapCheckCompress: "检查快照压缩文件"
|
||||
SnapCompressSize: "快照文件大小 {{ .name }}"
|
||||
SnapUpload: "上传快照文件"
|
||||
SnapLoadBackup: "获取备份账号信息"
|
||||
SnapUploadTo: "上传快照文件到 {{ .name }}"
|
||||
SnapUploadRes: "上传快照文件到 {{ .name }}"
|
||||
|
||||
SnapshotRecover: "快照恢复"
|
||||
RecoverDownload: "下载快照文件"
|
||||
Download: "下载"
|
||||
RecoverDownloadAccount: "获取快照下载备份账号 {{ .name }}"
|
||||
RecoverDecompress: "解压快照压缩文件"
|
||||
Decompress: "解压"
|
||||
BackupBeforeRecover: "快照前备份系统相关数据"
|
||||
Readjson: "读取快照内 Json 文件"
|
||||
ReadjsonPath: "获取快照内 Json 文件路径"
|
||||
ReadjsonContent: "读取 Json 文件"
|
||||
ReadjsonMarshal: "Json 转义处理"
|
||||
RecoverApp: "恢复已安装应用"
|
||||
RecoverAppImage: "恢复快照镜像备份"
|
||||
RecoverAppList: "获取所有待恢复应用"
|
||||
RecoverCompose: "恢复其他编排内容"
|
||||
RecoverComposeList: "获取所有待恢复编排"
|
||||
RecoverComposeItem: "恢复编排 {{ .name }}"
|
||||
RecoverAppEmpty: "快照文件中未发现应用镜像备份"
|
||||
RecoverBaseData: "恢复基础数据及文件"
|
||||
RecoverDaemonJsonEmpty: "快照文件及当前机器都不存在容器配置 daemon.json 文件"
|
||||
RecoverDaemonJson: "恢复容器配置 daemon.json 文件"
|
||||
RecoverDBData: "恢复数据库数据"
|
||||
RecoverBackups: "恢复本地备份目录"
|
||||
RecoverPanelData: "恢复数据目录"
|
||||
|
||||
|
|
|
|||
|
|
@ -66,40 +66,6 @@ func handleSnapStatus() {
|
|||
"rollback_status": constant.StatusFailed,
|
||||
"rollback_message": msgFailed,
|
||||
}).Error
|
||||
|
||||
snapRepo := repo.NewISnapshotRepo()
|
||||
|
||||
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.PanelData == constant.StatusRunning {
|
||||
updates["panel_data"] = constant.StatusFailed
|
||||
}
|
||||
if item.BackupData == constant.StatusRunning {
|
||||
updates["backup_data"] = constant.StatusFailed
|
||||
}
|
||||
if item.Compress == constant.StatusRunning {
|
||||
updates["compress"] = constant.StatusFailed
|
||||
}
|
||||
if item.Upload == constant.StatusUploading {
|
||||
updates["upload"] = constant.StatusFailed
|
||||
}
|
||||
if len(updates) != 0 {
|
||||
_ = snapRepo.UpdateStatus(item.ID, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleCronjobStatus() {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ func Init() {
|
|||
migrations.InitImageRepo,
|
||||
migrations.InitDefaultCA,
|
||||
migrations.InitPHPExtensions,
|
||||
migrations.AddTask,
|
||||
migrations.UpdateWebsite,
|
||||
migrations.UpdateWebsiteDomain,
|
||||
migrations.UpdateApp,
|
||||
migrations.AddTaskDB,
|
||||
migrations.UpdateAppInstall,
|
||||
migrations.UpdateSnapshot,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ var AddTable = &gormigrate.Migration{
|
|||
&model.Runtime{},
|
||||
&model.Setting{},
|
||||
&model.Snapshot{},
|
||||
&model.SnapshotStatus{},
|
||||
&model.Tag{},
|
||||
&model.Website{},
|
||||
&model.WebsiteAcmeAccount{},
|
||||
|
|
@ -211,3 +210,59 @@ var InitPHPExtensions = &gormigrate.Migration{
|
|||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddTask = &gormigrate.Migration{
|
||||
ID: "20240802-add-task",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.Task{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateWebsite = &gormigrate.Migration{
|
||||
ID: "20240812-update-website",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.Website{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateWebsiteDomain = &gormigrate.Migration{
|
||||
ID: "20240808-update-website-domain",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.WebsiteDomain{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddTaskDB = &gormigrate.Migration{
|
||||
ID: "20240822-add-task-table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return global.TaskDB.AutoMigrate(
|
||||
&model.Task{},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateApp = &gormigrate.Migration{
|
||||
ID: "20240826-update-app",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.App{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateAppInstall = &gormigrate.Migration{
|
||||
ID: "20240828-update-app-install",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.AppInstall{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateSnapshot = &gormigrate.Migration{
|
||||
ID: "20240926-update-snapshot",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.Snapshot{})
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ 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/recreate", baseApi.RecreateSnapshot)
|
||||
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
|
||||
settingRouter.POST("/snapshot/import", baseApi.ImportSnapshot)
|
||||
settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot)
|
||||
|
|
|
|||
|
|
@ -456,6 +456,47 @@ 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() {
|
||||
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+"/"))
|
||||
|
|
@ -670,3 +711,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"codemirror": "^6.0.1",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.5",
|
||||
"fit2cloud-ui-plus": "^1.1.5",
|
||||
"fit2cloud-ui-plus": "^1.1.7",
|
||||
"highlight.js": "^11.9.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"md-editor-v3": "^2.11.3",
|
||||
|
|
|
|||
|
|
@ -119,19 +119,27 @@ 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;
|
||||
backupAccountID: number;
|
||||
names: Array<string>;
|
||||
description: string;
|
||||
}
|
||||
export interface SnapshotRecover {
|
||||
id: number;
|
||||
taskID: string;
|
||||
isNew: boolean;
|
||||
reDownload: boolean;
|
||||
secret: string;
|
||||
|
|
@ -146,20 +154,43 @@ export namespace Setting {
|
|||
message: string;
|
||||
createdAt: DateTimeFormats;
|
||||
version: string;
|
||||
secret: string;
|
||||
|
||||
taskID: string;
|
||||
taskRecoverID: string;
|
||||
taskRollbackID: string;
|
||||
|
||||
interruptStep: string;
|
||||
recoverStatus: string;
|
||||
recoverMessage: string;
|
||||
lastRecoveredAt: string;
|
||||
rollbackStatus: string;
|
||||
rollbackMessage: string;
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export const searchBackupRecords = (params: Backup.SearchBackupRecord) => {
|
|||
export const searchBackupRecordsByCronjob = (params: Backup.SearchBackupRecordByCronjob) => {
|
||||
return http.post<ResPage<Backup.RecordInfo>>(`/backups/record/search/bycronjob`, params, TimeoutEnum.T_5M);
|
||||
};
|
||||
export const getFilesFromBackup = (type: string) => {
|
||||
return http.post<Array<any>>(`/backups/search/files`, { type: type });
|
||||
export const getFilesFromBackup = (id: number) => {
|
||||
return http.post<Array<any>>(`/backups/search/files`, { id: id });
|
||||
};
|
||||
|
||||
// backup-core
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export const loadBaseDir = () => {
|
|||
export const loadDaemonJsonPath = () => {
|
||||
return http.get<string>(`/settings/daemonjson`, {});
|
||||
};
|
||||
export const updateAgentSetting = (param: Setting.SettingUpdate) => {
|
||||
return http.post(`/settings/update`, param);
|
||||
};
|
||||
export const getAgentSettingInfo = () => {
|
||||
return http.post<Setting.SettingInfo>(`/settings/search`);
|
||||
};
|
||||
|
||||
// core
|
||||
export const getSettingInfo = () => {
|
||||
|
|
@ -88,9 +94,15 @@ export const bindMFA = (param: Setting.MFABind) => {
|
|||
};
|
||||
|
||||
// snapshot
|
||||
export const loadSnapshotInfo = () => {
|
||||
return http.get<Setting.SnapshotData>(`/settings/snapshot/load`);
|
||||
};
|
||||
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
|
||||
return http.post(`/settings/snapshot`, param);
|
||||
};
|
||||
export const snapshotRecreate = (id: number) => {
|
||||
return http.post(`/settings/snapshot/recreate`, { id: id });
|
||||
};
|
||||
export const loadSnapStatus = (id: number) => {
|
||||
return http.post<Setting.SnapshotStatus>(`/settings/snapshot/status`, { id: id });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const message = {
|
|||
false: 'false',
|
||||
example: 'e.g.:',
|
||||
button: {
|
||||
prev: 'Previous',
|
||||
next: 'Next',
|
||||
create: 'Create ',
|
||||
add: 'Add ',
|
||||
save: 'Save ',
|
||||
|
|
@ -1587,6 +1589,38 @@ 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',
|
||||
operationLog: 'Retain Operation Log',
|
||||
loginLog: 'Retain Access Log',
|
||||
systemLog: 'Retain System Log',
|
||||
taskLog: 'Retain Task Log',
|
||||
monitorData: 'Retain Monitoring Data',
|
||||
selectAllImage: 'Backup All Application Images',
|
||||
agentLabel: 'Node Configuration',
|
||||
appDataLabel: 'Application Data',
|
||||
appImage: 'Application Image',
|
||||
appBackup: 'Application Backup',
|
||||
backupLabel: 'Backup Directory',
|
||||
confLabel: 'Configuration File',
|
||||
dockerLabel: 'Container',
|
||||
taskLabel: 'Scheduled Task',
|
||||
resourceLabel: 'Application Resource Directory',
|
||||
runtimeLabel: 'Runtime Environment',
|
||||
appLabel: 'Application',
|
||||
databaseLabel: 'Database',
|
||||
snapshotLabel: 'Snapshot File',
|
||||
websiteLabel: 'Website',
|
||||
directoryLabel: 'Directory',
|
||||
appStoreLabel: 'Application Store',
|
||||
shellLabel: 'Script',
|
||||
tmpLabel: 'Temporary Directory',
|
||||
sslLabel: 'Certificate Directory',
|
||||
reCreate: 'Failed to create snapshot',
|
||||
reRollback: 'Rollback snapshot failed',
|
||||
deleteHelper:
|
||||
'All backup files for the snapshot, including those in the third-party backup account, will be deleted.',
|
||||
status: 'Snapshot status',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const message = {
|
|||
false: '否',
|
||||
example: '例:',
|
||||
button: {
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
create: '創建',
|
||||
add: '添加',
|
||||
save: '保存',
|
||||
|
|
@ -1405,6 +1407,38 @@ const message = {
|
|||
backupJump: '未在當前備份列表中的備份檔案,請嘗試從檔案目錄中下載後導入備份。',
|
||||
|
||||
snapshot: '快照',
|
||||
stepBaseData: '基礎數據',
|
||||
stepAppData: '系統應用',
|
||||
stepPanelData: '系統數據',
|
||||
stepBackupData: '備份數據',
|
||||
stepOtherData: '其他數據',
|
||||
operationLog: '保留操作日誌',
|
||||
loginLog: '保留訪問日誌',
|
||||
systemLog: '保留系統日誌',
|
||||
taskLog: '保留任務日誌',
|
||||
monitorData: '保留監控數據',
|
||||
selectAllImage: '備份所有應用鏡像',
|
||||
agentLabel: '節點配置',
|
||||
appDataLabel: '應用數據',
|
||||
appImage: '應用鏡像',
|
||||
appBackup: '應用備份',
|
||||
backupLabel: '備份目錄',
|
||||
confLabel: '配置文件',
|
||||
dockerLabel: '容器',
|
||||
taskLabel: '計劃任務',
|
||||
resourceLabel: '應用資源目錄',
|
||||
runtimeLabel: '運行環境',
|
||||
appLabel: '應用',
|
||||
databaseLabel: '數據庫',
|
||||
snapshotLabel: '快照文件',
|
||||
websiteLabel: '網站',
|
||||
directoryLabel: '目錄',
|
||||
appStoreLabel: '應用商店',
|
||||
shellLabel: '腳本',
|
||||
tmpLabel: '臨時目錄',
|
||||
sslLabel: '證書目錄',
|
||||
reCreate: '创建快照失败',
|
||||
reRollback: '回滾快照失敗',
|
||||
deleteHelper: '將刪除該快照的所有備份文件,包括第三方備份賬號中的文件。',
|
||||
status: '快照狀態',
|
||||
ignoreRule: '排除規則',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const message = {
|
|||
false: '否',
|
||||
example: '例:',
|
||||
button: {
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
create: '创建',
|
||||
add: '添加',
|
||||
save: '保存',
|
||||
|
|
@ -1407,6 +1409,34 @@ const message = {
|
|||
backupJump: '未在当前备份列表中的备份文件,请尝试从文件目录中下载后导入备份。',
|
||||
|
||||
snapshot: '快照',
|
||||
stepBaseData: '基础数据',
|
||||
stepAppData: '系统应用',
|
||||
stepPanelData: '系统数据',
|
||||
stepBackupData: '备份数据',
|
||||
stepOtherData: '其他数据',
|
||||
monitorData: '监控数据',
|
||||
selectAllImage: '备份所有应用镜像',
|
||||
agentLabel: '节点配置',
|
||||
appDataLabel: '应用数据',
|
||||
appImage: '应用镜像',
|
||||
appBackup: '应用备份',
|
||||
backupLabel: '备份目录',
|
||||
confLabel: '配置文件',
|
||||
dockerLabel: '容器',
|
||||
taskLabel: '计划任务',
|
||||
resourceLabel: '应用资源目录',
|
||||
runtimeLabel: '运行环境',
|
||||
appLabel: '应用',
|
||||
databaseLabel: '数据库',
|
||||
snapshotLabel: '快照文件',
|
||||
websiteLabel: '网站',
|
||||
directoryLabel: '目录',
|
||||
appStoreLabel: '应用商店',
|
||||
shellLabel: '脚本',
|
||||
tmpLabel: '临时目录',
|
||||
sslLabel: '证书目录',
|
||||
reCreate: '创建快照失败',
|
||||
reRollback: '回滚快照失败',
|
||||
deleteHelper: '将删除该快照的所有备份文件,包括第三方备份账号中的文件。',
|
||||
ignoreRule: '排除规则',
|
||||
ignoreHelper: '快照时将使用该规则对 1Panel 数据目录进行压缩备份,请谨慎修改。',
|
||||
|
|
@ -1421,13 +1451,14 @@ const message = {
|
|||
compress: '制作快照文件',
|
||||
upload: '上传快照文件',
|
||||
recoverDetail: '恢复详情',
|
||||
recoverFailed: '快照恢复失败',
|
||||
createSnapshot: '创建快照',
|
||||
importSnapshot: '同步快照',
|
||||
importHelper: '快照文件目录:',
|
||||
recover: '恢复',
|
||||
lastRecoverAt: '上次恢复时间',
|
||||
lastRollbackAt: '上次回滚时间',
|
||||
reDownload: '重新下载备份文件',
|
||||
reDownload: '重新下载',
|
||||
statusSuccess: '成功',
|
||||
statusFailed: '失败',
|
||||
recoverErrArch: '不支持在不同服务器架构之间进行快照恢复操作!',
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<TaskLog ref="taskLogRef" />
|
||||
<TaskLog ref="taskLogRef" width="70%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
544
frontend/src/views/setting/snapshot/create/index.vue
Normal file
544
frontend/src/views/setting/snapshot/create/index.vue
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.snapshot')" :back="handleClose" size="large">
|
||||
<fu-steps
|
||||
v-loading="loading"
|
||||
class="steps"
|
||||
:space="50"
|
||||
ref="stepsRef"
|
||||
direction="vertical"
|
||||
:isLoading="stepLoading"
|
||||
:finishButtonText="$t('commons.button.create')"
|
||||
@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')">
|
||||
<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"
|
||||
:data="form.appData"
|
||||
:props="defaultProps"
|
||||
@check-change="onChangeAppData"
|
||||
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>
|
||||
</fu-step>
|
||||
<fu-step id="panelData" :title="$t('setting.stepPanelData')">
|
||||
<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>
|
||||
</fu-step>
|
||||
<fu-step id="backupData" :title="$t('setting.stepBackupData')">
|
||||
<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>
|
||||
</fu-step>
|
||||
<fu-step id="otherData" :title="$t('setting.stepOtherData')">
|
||||
<div class="ml-5">
|
||||
<el-checkbox v-model="form.withOperationLog" :label="$t('logs.operation')" size="large" />
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<el-checkbox v-model="form.withLoginLog" :label="$t('logs.login')" size="large" />
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<el-checkbox v-model="form.withSystemLog" :label="$t('logs.system')" size="large" />
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<el-checkbox v-model="form.withTaskLog" :label="$t('logs.task')" size="large" />
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<el-checkbox v-model="form.withMonitorData" :label="$t('setting.monitorData')" size="large" />
|
||||
</div>
|
||||
</fu-step>
|
||||
<template #footer></template>
|
||||
</fu-steps>
|
||||
<template #footer>
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button @click="prev" v-if="nowIndex !== 0">{{ $t('commons.button.prev') }}</el-button>
|
||||
<el-button type="primary" v-if="nowIndex === 4" @click="submitAddSnapshot">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
<el-button @click="next" v-else>{{ $t('commons.button.next') }}</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<TaskLog ref="taskLogRef" width="70%" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { loadSnapshotInfo, snapshotCreate } from '@/api/modules/setting';
|
||||
import { computeSize, newUUID } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import TaskLog from '@/components/task-log/index.vue';
|
||||
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 nowIndex = ref(0);
|
||||
|
||||
const appRef = ref();
|
||||
const panelRef = ref();
|
||||
const backupRef = ref();
|
||||
const taskLogRef = ref();
|
||||
|
||||
const backupOptions = ref();
|
||||
const accountOptions = ref();
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
taskID: '',
|
||||
downloadAccountID: '',
|
||||
fromAccounts: [],
|
||||
sourceAccountIDs: '',
|
||||
description: '',
|
||||
secret: '',
|
||||
|
||||
backupAllImage: false,
|
||||
withLoginLog: false,
|
||||
withOperationLog: false,
|
||||
withSystemLog: false,
|
||||
withTaskLog: 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;
|
||||
}
|
||||
};
|
||||
|
||||
function next() {
|
||||
stepsRef.value.next();
|
||||
}
|
||||
function prev() {
|
||||
stepsRef.value.prev();
|
||||
}
|
||||
|
||||
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) => {
|
||||
nowIndex.value = currentStep.index;
|
||||
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 'docker':
|
||||
case 'log':
|
||||
case 'runtime':
|
||||
case 'task':
|
||||
case 'app':
|
||||
case 'database':
|
||||
case 'website':
|
||||
case 'directory':
|
||||
return i18n.global.t('setting.' + label + 'Label');
|
||||
case 'system_snapshot':
|
||||
return i18n.global.t('setting.snapshotLabel');
|
||||
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.taskID = newUUID();
|
||||
form.sourceAccountIDs = form.fromAccounts.join(',');
|
||||
await snapshotCreate(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
openTaskLog(form.taskID);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
};
|
||||
|
||||
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 loadSnapshotInfo();
|
||||
form.panelData = res.data.panelData || [];
|
||||
form.backupData = res.data.backupData || [];
|
||||
form.appData = res.data.appData || [];
|
||||
};
|
||||
|
||||
function onChangeAppData(data: any, isCheck: boolean) {
|
||||
if (data.label !== 'appData' || !data.relationItemID) {
|
||||
return;
|
||||
}
|
||||
data.isCheck = isCheck;
|
||||
let isDisable = false;
|
||||
for (const item of form.appData) {
|
||||
if (!item.children) {
|
||||
return;
|
||||
}
|
||||
for (const itemData of item.children) {
|
||||
if (itemData.label === 'appData' && itemData.relationItemID === data.relationItemID && itemData.isCheck) {
|
||||
isDisable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const item of form.appData) {
|
||||
if (!item.children) {
|
||||
return;
|
||||
}
|
||||
for (const relationItem of item.children) {
|
||||
if (relationItem.id !== data.relationItemID) {
|
||||
continue;
|
||||
}
|
||||
relationItem.isDisable = isDisable;
|
||||
if (isDisable) {
|
||||
appRef.value.setChecked(relationItem.id, isDisable, isDisable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
|
@ -44,7 +44,7 @@ import i18n from '@/lang';
|
|||
import { MsgSuccess } from '@/utils/message';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { getSettingInfo, loadBaseDir, updateSetting } from '@/api/modules/setting';
|
||||
import { getAgentSettingInfo, loadBaseDir, updateAgentSetting } from '@/api/modules/setting';
|
||||
|
||||
const loading = ref();
|
||||
const baseDir = ref();
|
||||
|
|
@ -78,7 +78,7 @@ function checkData(rule: any, value: any, callback: any) {
|
|||
|
||||
const acceptParams = async (): Promise<void> => {
|
||||
loadPath();
|
||||
const res = await getSettingInfo();
|
||||
const res = await getAgentSettingInfo();
|
||||
tableList.value = [];
|
||||
let items = res.data.snapshotIgnore.split(',');
|
||||
for (const item of items) {
|
||||
|
|
@ -118,7 +118,7 @@ const onSave = async () => {
|
|||
for (const item of tableList.value) {
|
||||
list.push(item.value);
|
||||
}
|
||||
await updateSetting({ key: 'SnapshotIgnore', value: list.join(',') })
|
||||
await updateAgentSetting({ key: 'SnapshotIgnore', value: list.join(',') })
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
loading.value = false;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,10 @@
|
|||
<DrawerPro v-model="drawerVisible" :header="$t('setting.importSnapshot')" :back="handleClose" size="small">
|
||||
<el-form ref="formRef" label-position="top" :model="form" :rules="rules" v-loading="loading">
|
||||
<el-form-item :label="$t('setting.backupAccount')" prop="from">
|
||||
<el-select v-model="form.from" @change="loadFiles" clearable>
|
||||
<el-option
|
||||
v-for="item in backupOptions"
|
||||
:key="item.label"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
<el-select v-model="form.backupAccountID" @change="loadFiles" clearable>
|
||||
<el-option v-for="item in backupOptions" :key="item.label" :value="item.id" :label="item.label" />
|
||||
</el-select>
|
||||
<div v-if="form.from === 'LOCAL'">
|
||||
<div v-if="form.backupAccountID === 0">
|
||||
<span class="import-help">{{ $t('setting.importHelper') }}</span>
|
||||
<span @click="toFolder()" class="import-link-help">{{ backupPath }}</span>
|
||||
</div>
|
||||
|
|
@ -61,13 +56,13 @@ const existNames = ref();
|
|||
const backupPath = ref('');
|
||||
|
||||
const form = reactive({
|
||||
from: '',
|
||||
backupAccountID: 0,
|
||||
names: [],
|
||||
description: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
from: [Rules.requiredSelect],
|
||||
backupAccountID: [Rules.requiredSelect],
|
||||
names: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
|
|
@ -76,7 +71,7 @@ interface DialogProps {
|
|||
}
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
form.from = '';
|
||||
form.backupAccountID = undefined;
|
||||
existNames.value = params.names;
|
||||
form.names = [] as Array<string>;
|
||||
loadBackups();
|
||||
|
|
@ -125,7 +120,11 @@ const loadBackups = async () => {
|
|||
loading.value = false;
|
||||
backupOptions.value = [];
|
||||
for (const item of res.data) {
|
||||
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
|
||||
backupOptions.value.push({
|
||||
id: item.id,
|
||||
label: i18n.global.t('setting.' + item.type),
|
||||
value: item.type,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -135,7 +134,7 @@ const loadBackups = async () => {
|
|||
|
||||
const loadFiles = async () => {
|
||||
form.names = [];
|
||||
const res = await getFilesFromBackup(form.from);
|
||||
const res = await getFilesFromBackup(form.backupAccountID);
|
||||
fileNames.value = res.data || [];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -77,20 +77,49 @@
|
|||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'Waiting' || row.status === 'OnSaveData'"
|
||||
type="primary"
|
||||
@click="onLoadStatus(row)"
|
||||
link
|
||||
>
|
||||
{{ $t('commons.table.statusWaiting') }}
|
||||
</el-button>
|
||||
<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">
|
||||
{{ $t('commons.status.success') }}
|
||||
</el-tag>
|
||||
<div>
|
||||
<el-button link v-if="row.status === 'Waiting'" type="primary">
|
||||
{{ $t('setting.snapshot') }}{{ $t('commons.table.statusWaiting') }}
|
||||
</el-button>
|
||||
<el-button link v-if="row.status === 'Failed'" @click="reCreate(row)" type="danger">
|
||||
{{ $t('setting.snapshot') }}{{ $t('commons.status.error') }}
|
||||
</el-button>
|
||||
<el-button link v-if="row.status === 'Success'" type="success">
|
||||
{{ $t('setting.snapshot') }}{{ $t('commons.status.success') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.recoverStatus">
|
||||
<el-button link v-if="row.recoverStatus === 'Waiting'" type="primary">
|
||||
{{ $t('commons.button.recover') }}{{ $t('commons.table.statusWaiting') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.recoverStatus === 'Failed'"
|
||||
@click="onRecover(row)"
|
||||
type="danger"
|
||||
link
|
||||
>
|
||||
{{ $t('commons.button.recover') }}{{ $t('commons.status.error') }}
|
||||
</el-button>
|
||||
<el-button link v-if="row.recoverStatus === 'Success'" type="success">
|
||||
{{ $t('commons.button.recover') }}{{ $t('commons.status.success') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.rollbackStatus">
|
||||
<el-button link v-if="row.rollbackStatus === 'Waiting'" type="primary">
|
||||
{{ $t('setting.rollback') }}{{ $t('commons.table.statusWaiting') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
v-if="row.rollbackStatus === 'Failed'"
|
||||
@click="reRollback(row)"
|
||||
type="danger"
|
||||
>
|
||||
{{ $t('setting.rollback') }}{{ $t('commons.status.error') }}
|
||||
</el-button>
|
||||
<el-button link v-if="row.recoverStatus === 'Success'" type="success">
|
||||
{{ $t('setting.rollback') }}{{ $t('commons.status.success') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.description')" prop="description" show-overflow-tooltip>
|
||||
|
|
@ -105,7 +134,7 @@
|
|||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
width="200px"
|
||||
width="240px"
|
||||
:ellipsis="10"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
|
|
@ -115,52 +144,9 @@
|
|||
</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>
|
||||
<IgnoreRule ref="ignoreRef" @search="search()" />
|
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
|
|
@ -174,25 +160,32 @@
|
|||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
<SnapStatus ref="snapStatusRef" @search="search" />
|
||||
<IgnoreRule ref="ignoreRef" />
|
||||
<TaskLog ref="taskLogRef" width="70%" />
|
||||
<SnapRecover ref="recoverRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { snapshotCreate, searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
|
||||
import {
|
||||
searchSnapshotPage,
|
||||
snapshotDelete,
|
||||
snapshotRecreate,
|
||||
snapshotRollback,
|
||||
updateSnapshotDescription,
|
||||
} from '@/api/modules/setting';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { computeSize, dateFormat } from '@/utils/util';
|
||||
import { computeSize, dateFormat, newUUID } 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 TaskLog from '@/components/task-log/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 SnapRecover from '@/views/setting/snapshot/recover/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { loadOsInfo } from '@/api/modules/dashboard';
|
||||
|
||||
const loading = ref(false);
|
||||
const data = ref();
|
||||
|
|
@ -206,41 +199,18 @@ const paginationConfig = reactive({
|
|||
const searchName = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const ignoreRef = ref();
|
||||
|
||||
const snapStatusRef = ref();
|
||||
const createRef = ref();
|
||||
const ignoreRef = ref();
|
||||
const recoverStatusRef = ref();
|
||||
const importRef = ref();
|
||||
const isRecordShow = ref();
|
||||
const backupOptions = ref();
|
||||
const accountOptions = ref();
|
||||
const taskLogRef = ref();
|
||||
const recoverRef = 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,30 +219,48 @@ const onImport = () => {
|
|||
importRef.value.acceptParams({ names: names });
|
||||
};
|
||||
|
||||
const onIgnore = () => {
|
||||
ignoreRef.value.acceptParams();
|
||||
const onCreate = () => {
|
||||
createRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const onChange = async (info: any) => {
|
||||
await updateSnapshotDescription({ id: info.id, description: info.description });
|
||||
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)
|
||||
const reCreate = (row: any) => {
|
||||
ElMessageBox.confirm(row.message, i18n.global.t('setting.reCreate'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.retry'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'error',
|
||||
}).then(async () => {
|
||||
await snapshotRecreate(row.id)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
search();
|
||||
openTaskLog(row.taskID);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
};
|
||||
|
||||
const reRollback = (row: any) => {
|
||||
ElMessageBox.confirm(row.rollbackMessage, i18n.global.t('setting.reRollback'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.retry'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'error',
|
||||
}).then(async () => {
|
||||
let param = {
|
||||
id: row.id,
|
||||
taskID: newUUID(),
|
||||
isNew: false,
|
||||
reDownload: false,
|
||||
secret: '',
|
||||
};
|
||||
await snapshotRollback(param)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
openTaskLog(row.taskRollbackID || param.taskID);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -281,47 +269,41 @@ const submitAddSnapshot = (formEl: FormInstance | undefined) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onLoadStatus = (row: Setting.SnapshotInfo) => {
|
||||
snapStatusRef.value.acceptParams({
|
||||
id: row.id,
|
||||
from: row.from,
|
||||
defaultDownload: row.defaultDownload,
|
||||
description: row.description,
|
||||
});
|
||||
const onIgnore = () => {
|
||||
ignoreRef.value.acceptParams();
|
||||
};
|
||||
|
||||
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 onChange = async (info: any) => {
|
||||
await updateSnapshotDescription({ id: info.id, description: info.description });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
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 onRecover = async (row: any) => {
|
||||
loading.value = true;
|
||||
await loadOsInfo()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
let params = {
|
||||
id: row.id,
|
||||
taskID: row.taskRecoverID,
|
||||
isNew: row.recoverStatus === '',
|
||||
name: row.name,
|
||||
reDownload: false,
|
||||
secret: row.secret,
|
||||
|
||||
arch: res.data.kernelArch,
|
||||
size: row.size,
|
||||
freeSize: res.data.diskSize,
|
||||
|
||||
interruptStep: row.interruptStep,
|
||||
status: row.recoverStatus,
|
||||
message: row.recoverMessage,
|
||||
};
|
||||
recoverRef.value.acceptParams(params);
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const batchDelete = async (row: Setting.SnapshotInfo | null) => {
|
||||
|
|
@ -362,17 +344,11 @@ const onSubmitDelete = async () => {
|
|||
});
|
||||
};
|
||||
|
||||
function restForm() {
|
||||
if (snapRef.value) {
|
||||
snapRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
icon: 'RefreshLeft',
|
||||
click: (row: any) => {
|
||||
recoverStatusRef.value.acceptParams({ snapInfo: row });
|
||||
onRecover(row);
|
||||
},
|
||||
disabled: (row: any) => {
|
||||
return !(row.status === 'Success');
|
||||
|
|
@ -406,6 +382,5 @@ const search = async () => {
|
|||
|
||||
onMounted(() => {
|
||||
search();
|
||||
loadBackups();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
:before-close="handleClose"
|
||||
>
|
||||
<el-form ref="recoverForm" label-position="top" v-loading="loading">
|
||||
{{ $t('setting.recoverHelper', [recoverReq.name]) }}
|
||||
<div style="margin-left: 20px; line-height: 32px">
|
||||
<div style="margin-left: 20px; line-height: 32px" v-if="recoverReq.isNew">
|
||||
{{ $t('setting.recoverHelper', [recoverReq.name]) }}
|
||||
<div>
|
||||
<el-button style="margin-top: -4px" type="warning" link icon="WarningFilled" />
|
||||
{{ $t('setting.recoverHelper1') }}
|
||||
|
|
@ -32,10 +32,13 @@
|
|||
{{ $t('setting.recoverHelper3', [recoverReq.arch]) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item v-if="!recoverReq.isNew" class="mt-2">
|
||||
<el-form-item v-if="!recoverReq.isNew" :label="$t('setting.recoverFailed')">
|
||||
<span>{{ recoverReq.message }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!recoverReq.isNew" :label="$t('setting.snapshotLabel')">
|
||||
<el-checkbox v-model="recoverReq.reDownload">{{ $t('setting.reDownload') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.compressPassword')" class="mt-2">
|
||||
<el-form-item :label="$t('setting.compressPassword')">
|
||||
<el-input v-model="recoverReq.secret" :placeholder="$t('setting.backupRecoverMessage')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
|
@ -44,12 +47,19 @@
|
|||
<el-button @click="handleClose" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" :disabled="loading">
|
||||
<el-button @click="onRollback" v-if="canRollback()" :disabled="loading">
|
||||
{{ $t('setting.rollback') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" v-if="!recoverReq.isNew" :disabled="loading">
|
||||
{{ $t('commons.button.retry') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" v-if="recoverReq.isNew" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<TaskLog ref="taskLogRef" width="70%" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -57,33 +67,44 @@ import { ref } from 'vue';
|
|||
import { FormInstance } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { snapshotRecover } from '@/api/modules/setting';
|
||||
import { computeSize } from '@/utils/util';
|
||||
import TaskLog from '@/components/task-log/index.vue';
|
||||
import { snapshotRecover, snapshotRollback } from '@/api/modules/setting';
|
||||
import { computeSize, newUUID } from '@/utils/util';
|
||||
|
||||
let loading = ref(false);
|
||||
let open = ref(false);
|
||||
const recoverForm = ref<FormInstance>();
|
||||
const emit = defineEmits<{ (e: 'search'): void; (e: 'close'): void }>();
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const taskLogRef = ref();
|
||||
|
||||
interface DialogProps {
|
||||
id: number;
|
||||
isNew: boolean;
|
||||
name: string;
|
||||
taskID: string;
|
||||
reDownload: boolean;
|
||||
arch: string;
|
||||
size: number;
|
||||
freeSize: number;
|
||||
interruptStep: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
let recoverReq = ref({
|
||||
id: 0,
|
||||
isNew: true,
|
||||
name: '',
|
||||
taskID: '',
|
||||
reDownload: true,
|
||||
secret: '',
|
||||
arch: '',
|
||||
size: 0,
|
||||
freeSize: 0,
|
||||
interruptStep: '',
|
||||
status: '',
|
||||
message: '',
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
|
|
@ -94,11 +115,15 @@ const acceptParams = (params: DialogProps): void => {
|
|||
id: params.id,
|
||||
isNew: params.isNew,
|
||||
name: params.name,
|
||||
taskID: params.taskID,
|
||||
reDownload: params.reDownload,
|
||||
secret: '',
|
||||
arch: params.arch,
|
||||
size: params.size,
|
||||
freeSize: params.freeSize,
|
||||
interruptStep: params.interruptStep,
|
||||
status: params.status,
|
||||
message: params.message,
|
||||
};
|
||||
open.value = true;
|
||||
};
|
||||
|
|
@ -116,25 +141,66 @@ const isArchOk = () => {
|
|||
return recoverReq.value.name.indexOf(recoverReq.value.arch) !== -1;
|
||||
};
|
||||
|
||||
const canRollback = () => {
|
||||
return (
|
||||
!recoverReq.value.isNew &&
|
||||
recoverReq.value.interruptStep !== '' &&
|
||||
recoverReq.value.interruptStep !== 'RecoverDownload' &&
|
||||
recoverReq.value.interruptStep !== 'RecoverDecompress' &&
|
||||
recoverReq.value.interruptStep !== 'BackupBeforeRecover'
|
||||
);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
await snapshotRecover({
|
||||
let param = {
|
||||
id: recoverReq.value.id,
|
||||
taskID: newUUID(),
|
||||
isNew: recoverReq.value.isNew,
|
||||
reDownload: recoverReq.value.reDownload,
|
||||
secret: recoverReq.value.secret,
|
||||
})
|
||||
};
|
||||
await snapshotRecover(param)
|
||||
.then(() => {
|
||||
emit('search');
|
||||
loading.value = false;
|
||||
handleClose();
|
||||
emit('close');
|
||||
openTaskLog(recoverReq.value.taskID || param.taskID);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
};
|
||||
|
||||
const onRollback = 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: recoverReq.value.id,
|
||||
taskID: newUUID(),
|
||||
isNew: false,
|
||||
reDownload: false,
|
||||
secret: '',
|
||||
})
|
||||
.then(() => {
|
||||
emit('search');
|
||||
handleClose();
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
|
|
|
|||
|
|
@ -1,320 +0,0 @@
|
|||
<template>
|
||||
<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.panelInfo)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panelInfo)" link>{{ $t('setting.panelInfo') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panelInfo)" class="top-margin">
|
||||
<span class="err-message">{{ status.panelInfo }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.panel)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panel)" link>{{ $t('setting.panelBin') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panel)" class="top-margin">
|
||||
<span class="err-message">{{ status.panel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.daemonJson)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.daemonJson)" link>{{ $t('setting.daemonJson') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.daemonJson)" class="top-margin">
|
||||
<span class="err-message">{{ status.daemonJson }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.appData)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.appData)" link>{{ $t('setting.appData') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.appData)" class="top-margin">
|
||||
<span class="err-message">{{ status.appData }}</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>
|
||||
<div v-if="showErrorMsg(status.compress)" class="top-margin">
|
||||
<span class="err-message">{{ status.compress }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</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 lang="ts" setup>
|
||||
import { Setting } from '@/api/interface/setting';
|
||||
import { loadSnapStatus, snapshotCreate } from '@/api/modules/setting';
|
||||
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
|
||||
const status = reactive<Setting.SnapshotStatus>({
|
||||
panel: '',
|
||||
panelInfo: '',
|
||||
daemonJson: '',
|
||||
appData: '',
|
||||
panelData: '',
|
||||
backupData: '',
|
||||
|
||||
compress: '',
|
||||
size: '',
|
||||
upload: '',
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const loading = ref();
|
||||
const snapID = ref();
|
||||
const snapFrom = ref();
|
||||
const snapDefaultDownload = ref();
|
||||
const snapDescription = ref();
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
interface DialogProps {
|
||||
id: number;
|
||||
from: string;
|
||||
defaultDownload: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
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 loadCurrentStatus = async () => {
|
||||
loading.value = true;
|
||||
await loadSnapStatus(snapID.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
status.panel = res.data.panel;
|
||||
status.panelInfo = res.data.panelInfo;
|
||||
status.daemonJson = res.data.daemonJson;
|
||||
status.appData = res.data.appData;
|
||||
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;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
emit('search');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const onRetry = async () => {
|
||||
loading.value = true;
|
||||
await snapshotCreate({
|
||||
id: snapID.value,
|
||||
fromAccounts: [],
|
||||
from: snapFrom.value,
|
||||
defaultDownload: snapDefaultDownload.value,
|
||||
description: snapDescription.value,
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
loadCurrentStatus();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onWatch = () => {
|
||||
timer = setInterval(async () => {
|
||||
if (keepLoadStatus()) {
|
||||
const res = await loadSnapStatus(snapID.value);
|
||||
status.panel = res.data.panel;
|
||||
status.panelInfo = res.data.panelInfo;
|
||||
status.daemonJson = res.data.daemonJson;
|
||||
status.appData = res.data.appData;
|
||||
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.panel === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelInfo === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.daemonJson === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.appData === '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.panel !== 'Running' && status.panel !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelInfo !== 'Running' && status.panelInfo !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.daemonJson !== 'Running' && status.daemonJson !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.appData !== 'Running' && status.appData !== '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 scoped lang="scss">
|
||||
.el-alert {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.el-alert:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
.top-margin {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.err-message {
|
||||
margin-left: 23px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Reference in a new issue