mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 重构快照功能,取消快照过程 loading (#2039)
This commit is contained in:
parent
f9bf6b69fb
commit
f196d029cb
@ -60,6 +60,32 @@ 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 := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
if err := global.VALID.Struct(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
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 更新快照描述信息
|
||||
|
@ -75,7 +75,22 @@ type PortUpdate struct {
|
||||
ServerPort uint `json:"serverPort" validate:"required,number,max=65535,min=1"`
|
||||
}
|
||||
|
||||
type SnapshotStatus struct {
|
||||
Panel string `json:"panel"`
|
||||
PanelCtl string `json:"panelCtl"`
|
||||
PanelService string `json:"panelService"`
|
||||
PanelInfo string `json:"panelInfo"`
|
||||
DaemonJson string `json:"daemonJson"`
|
||||
AppData string `json:"appData"`
|
||||
PanelData string `json:"panelData"`
|
||||
BackupData string `json:"backupData"`
|
||||
|
||||
Compress string `json:"compress"`
|
||||
Upload string `json:"upload"`
|
||||
}
|
||||
|
||||
type SnapshotCreate struct {
|
||||
ID uint `json:"id"`
|
||||
From string `json:"from" validate:"required,oneof=OSS S3 SFTP MINIO COS KODO OneDrive"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
}
|
||||
|
@ -17,3 +17,19 @@ type Snapshot struct {
|
||||
RollbackMessage string `json:"rollbackMessage" gorm:"type:varchar(256)"`
|
||||
LastRollbackedAt string `json:"lastRollbackedAt" gorm:"type:varchar(64)"`
|
||||
}
|
||||
|
||||
type SnapshotStatus struct {
|
||||
BaseModel
|
||||
SnapID uint `gorm:"type:decimal" json:"snapID"`
|
||||
Panel string `json:"panel" gorm:"type:varchar(64);default:Running"`
|
||||
PanelCtl string `json:"panelCtl" gorm:"type:varchar(64);default:Running"`
|
||||
PanelService string `json:"panelService" gorm:"type:varchar(64);default:Running"`
|
||||
PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"`
|
||||
DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"`
|
||||
AppData string `json:"appData" gorm:"type:varchar(64);default:Running"`
|
||||
PanelData string `json:"panelData" gorm:"type:varchar(64);default:Running"`
|
||||
BackupData string `json:"backupData" gorm:"type:varchar(64);default:Running"`
|
||||
|
||||
Compress string `json:"compress" gorm:"type:varchar(64);default:Waiting"`
|
||||
Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"`
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -20,6 +22,7 @@ type IRuntimeRepo interface {
|
||||
Save(runtime *model.Runtime) error
|
||||
DeleteBy(opts ...DBOption) error
|
||||
GetFirst(opts ...DBOption) (*model.Runtime, error)
|
||||
List(opts ...DBOption) ([]model.Runtime, error)
|
||||
}
|
||||
|
||||
func NewIRunTimeRepo() IRuntimeRepo {
|
||||
@ -65,6 +68,16 @@ func (r *RuntimeRepo) Page(page, size int, opts ...DBOption) (int64, []model.Run
|
||||
return count, runtimes, err
|
||||
}
|
||||
|
||||
func (r *RuntimeRepo) List(opts ...DBOption) ([]model.Runtime, error) {
|
||||
var runtimes []model.Runtime
|
||||
db := global.DB.Model(&model.Runtime{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&runtimes).Error
|
||||
return runtimes, err
|
||||
}
|
||||
|
||||
func (r *RuntimeRepo) Create(ctx context.Context, runtime *model.Runtime) error {
|
||||
db := getTx(ctx).Model(&model.Runtime{})
|
||||
return db.Create(&runtime).Error
|
||||
|
@ -12,6 +12,12 @@ 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 {
|
||||
@ -67,3 +73,33 @@ 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
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error
|
||||
Username: db.Username,
|
||||
Permission: db.Permission,
|
||||
Timeout: 300,
|
||||
}); err != nil {
|
||||
}); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
@ -34,6 +35,8 @@ type ISnapshotService interface {
|
||||
SnapshotImport(req dto.SnapshotImport) error
|
||||
Delete(req dto.BatchDeleteReq) error
|
||||
|
||||
LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error)
|
||||
|
||||
UpdateDescription(req dto.UpdateDescription) error
|
||||
readFromJson(path string) (SnapshotJson, error)
|
||||
}
|
||||
@ -99,6 +102,18 @@ 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"`
|
||||
@ -113,112 +128,103 @@ type SnapshotJson struct {
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
|
||||
global.LOG.Info("start to create snapshot now")
|
||||
localDir, err := loadLocalDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup, err := backupRepo.Get(commonRepo.WithByType(req.From))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupAccount, err := NewIBackupService().NewClient(&backup)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
var (
|
||||
snap model.Snapshot
|
||||
snapStatus model.SnapshotStatus
|
||||
rootDir string
|
||||
)
|
||||
|
||||
if req.ID == 0 {
|
||||
timeNow := time.Now().Format("20060102150405")
|
||||
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
|
||||
rootDir = path.Join(localDir, fmt.Sprintf("system/1panel_%s_%s", versionItem.Value, timeNow))
|
||||
|
||||
snap = model.Snapshot{
|
||||
Name: fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow),
|
||||
Description: req.Description,
|
||||
From: req.From,
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusWaiting,
|
||||
}
|
||||
_ = snapshotRepo.Create(&snap)
|
||||
snapStatus.SnapID = snap.ID
|
||||
_ = snapshotRepo.CreateStatus(&snapStatus)
|
||||
} else {
|
||||
snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapStatus, _ = snapshotRepo.GetStatus(snap.ID)
|
||||
if snapStatus.ID == 0 {
|
||||
snapStatus.SnapID = snap.ID
|
||||
_ = snapshotRepo.CreateStatus(&snapStatus)
|
||||
}
|
||||
rootDir = path.Join(localDir, fmt.Sprintf("system/%s", snap.Name))
|
||||
}
|
||||
|
||||
timeNow := time.Now().Format("20060102150405")
|
||||
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
|
||||
rootDir := path.Join(localDir, fmt.Sprintf("system/1panel_%s_%s", versionItem.Value, timeNow))
|
||||
backupPanelDir := fmt.Sprintf("%s/1panel", rootDir)
|
||||
var wg sync.WaitGroup
|
||||
itemHelper := snapHelper{SnapID: snap.ID, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
|
||||
backupPanelDir := path.Join(rootDir, "1panel")
|
||||
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
|
||||
backupDockerDir := fmt.Sprintf("%s/docker", rootDir)
|
||||
backupDockerDir := path.Join(rootDir, "docker")
|
||||
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
|
||||
|
||||
_ = settingRepo.Update("SystemStatus", "Snapshoting")
|
||||
snap := model.Snapshot{
|
||||
Name: fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow),
|
||||
Description: req.Description,
|
||||
From: req.From,
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusSuccess,
|
||||
jsonItem := SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: localDir,
|
||||
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
|
||||
}
|
||||
_ = snapshotRepo.Create(&snap)
|
||||
|
||||
if snapStatus.PanelInfo != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapJson(itemHelper, snapStatus.ID, jsonItem, rootDir)
|
||||
}
|
||||
if snapStatus.Panel != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanel(itemHelper, snapStatus.ID, backupPanelDir)
|
||||
}
|
||||
if snapStatus.PanelCtl != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanelCtl(itemHelper, snapStatus.ID, backupPanelDir)
|
||||
}
|
||||
if snapStatus.PanelService != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanelService(itemHelper, snapStatus.ID, backupPanelDir)
|
||||
}
|
||||
if snapStatus.DaemonJson != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapDaemonJson(itemHelper, snapStatus.ID, backupDockerDir)
|
||||
}
|
||||
if snapStatus.AppData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapAppData(itemHelper, snapStatus.ID, backupDockerDir)
|
||||
}
|
||||
if snapStatus.BackupData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapBackup(itemHelper, snapStatus.ID, localDir, backupPanelDir)
|
||||
}
|
||||
if snapStatus.PanelData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanelData(itemHelper, snapStatus.ID, localDir, backupPanelDir)
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = global.Cron.Stop()
|
||||
defer func() {
|
||||
global.Cron.Start()
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}()
|
||||
fileOp := files.NewFileOp()
|
||||
wg.Wait()
|
||||
if checkIsAllDone(snap.ID) {
|
||||
snapCompress(itemHelper, snapStatus.ID, rootDir)
|
||||
|
||||
snapJson := SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: localDir,
|
||||
snapUpload(req.From, snapStatus.ID, fmt.Sprintf("%s.tar.gz", rootDir))
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
} else {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
}
|
||||
|
||||
if err := u.handleDockerDatasWithSave(fileOp, "snapshot", "", backupDockerDir); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
if err := u.handleDaemonJson(fileOp, "snapshot", "", backupDockerDir); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.handlePanelBinary(fileOp, "snapshot", "", backupPanelDir+"/1panel"); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
if err := u.handlePanelctlBinary(fileOp, "snapshot", "", backupPanelDir+"/1pctl"); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
if err := u.handlePanelService(fileOp, "snapshot", "", backupPanelDir+"/1panel.service"); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.handleBackupDatas(fileOp, "snapshot", localDir, backupPanelDir); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dataDir := path.Join(global.CONF.System.BaseDir, "1panel")
|
||||
if err := u.handlePanelDatas(snap.ID, fileOp, "snapshot", dataDir, backupPanelDir, localDir, snapJson.DockerDataDir); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl restart docker")
|
||||
snapJson.PanelDataDir = dataDir
|
||||
|
||||
if err := u.saveJson(snapJson, rootDir); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("save snapshot json failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleTar(rootDir, path.Join(localDir, "system"), fmt.Sprintf("1panel_%s_%s.tar.gz", versionItem.Value, timeNow), ""); err != nil {
|
||||
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
|
||||
global.LOG.Infof("start to upload snapshot to %s, please wait", backup.Type)
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusUploading})
|
||||
localPath := path.Join(localDir, fmt.Sprintf("system/1panel_%s_%s.tar.gz", versionItem.Value, timeNow))
|
||||
itemBackupPath := strings.TrimPrefix(backup.BackupPath, "/")
|
||||
itemBackupPath = strings.TrimSuffix(itemBackupPath, "/")
|
||||
if ok, err := backupAccount.Upload(localPath, fmt.Sprintf("%s/system_snapshot/1panel_%s_%s.tar.gz", itemBackupPath, versionItem.Value, timeNow)); err != nil || !ok {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err)
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
_ = os.RemoveAll(path.Join(localDir, fmt.Sprintf("system/1panel_%s_%s.tar.gz", versionItem.Value, timeNow)))
|
||||
|
||||
global.LOG.Infof("upload snapshot to %s success", backup.Type)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -556,11 +562,11 @@ func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) {
|
||||
func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error {
|
||||
switch operation {
|
||||
case "snapshot":
|
||||
if err := u.handleTar(source, target, "docker_data.tar.gz", ""); err != nil {
|
||||
if err := handleSnapTar(source, target, "docker_data.tar.gz", ""); err != nil {
|
||||
return fmt.Errorf("backup docker data failed, err: %v", err)
|
||||
}
|
||||
case "recover":
|
||||
if err := u.handleTar(target, u.OriginalPath, "docker_data.tar.gz", ""); err != nil {
|
||||
if err := handleSnapTar(target, u.OriginalPath, "docker_data.tar.gz", ""); err != nil {
|
||||
return fmt.Errorf("backup docker data failed, err: %v", err)
|
||||
}
|
||||
if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil {
|
||||
@ -581,31 +587,6 @@ func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation strin
|
||||
|
||||
func (u *SnapshotService) handleDockerDatasWithSave(fileOp files.FileOp, operation, source, target string) error {
|
||||
switch operation {
|
||||
case "snapshot":
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRegex := regexp.MustCompile(`image:\s*(.*)`)
|
||||
var imageSaveList []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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(target, "docker_image.tar"))
|
||||
if err != nil {
|
||||
return errors.New(std)
|
||||
}
|
||||
case "recover":
|
||||
if err := u.handleDockerDatasWithSave(fileOp, "snapshot", "", u.OriginalPath); err != nil {
|
||||
return fmt.Errorf("backup docker data failed, err: %v", err)
|
||||
@ -731,12 +712,8 @@ func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation stri
|
||||
|
||||
func (u *SnapshotService) handleBackupDatas(fileOp files.FileOp, operation string, source, target string) error {
|
||||
switch operation {
|
||||
case "snapshot":
|
||||
if err := u.handleTar(source, target, "1panel_backup.tar.gz", "./system;"); err != nil {
|
||||
return fmt.Errorf("backup panel local backup dir data failed, err: %v", err)
|
||||
}
|
||||
case "recover":
|
||||
if err := u.handleTar(target, u.OriginalPath, "1panel_backup.tar.gz", "./system;"); err != nil {
|
||||
if err := handleSnapTar(target, u.OriginalPath, "1panel_backup.tar.gz", "./system;"); err != nil {
|
||||
return fmt.Errorf("restore original local backup dir data failed, err: %v", err)
|
||||
}
|
||||
if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil {
|
||||
@ -766,7 +743,7 @@ func (u *SnapshotService) handlePanelDatas(snapID uint, fileOp files.FileOp, ope
|
||||
exclusionRules += ("." + strings.ReplaceAll(dockerDir, source, "") + ";")
|
||||
}
|
||||
|
||||
if err := u.handleTar(source, target, "1panel_data.tar.gz", exclusionRules); err != nil {
|
||||
if err := handleSnapTar(source, target, "1panel_data.tar.gz", exclusionRules); err != nil {
|
||||
return fmt.Errorf("backup panel data failed, err: %v", err)
|
||||
}
|
||||
case "recover":
|
||||
@ -779,7 +756,7 @@ func (u *SnapshotService) handlePanelDatas(snapID uint, fileOp files.FileOp, ope
|
||||
}
|
||||
|
||||
_ = snapshotRepo.Update(snapID, map[string]interface{}{"recover_status": ""})
|
||||
if err := u.handleTar(target, u.OriginalPath, "1panel_data.tar.gz", exclusionRules); err != nil {
|
||||
if err := handleSnapTar(target, u.OriginalPath, "1panel_data.tar.gz", exclusionRules); err != nil {
|
||||
return fmt.Errorf("restore original panel data failed, err: %v", err)
|
||||
}
|
||||
_ = snapshotRepo.Update(snapID, map[string]interface{}{"recover_status": constant.StatusWaiting})
|
||||
@ -825,6 +802,7 @@ func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
|
||||
if _, err := os.Stat(itemFile); err == nil {
|
||||
_ = os.Remove(itemFile)
|
||||
}
|
||||
_ = snapshotRepo.DeleteStatus(snap.ID)
|
||||
}
|
||||
if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
|
||||
return err
|
||||
@ -833,18 +811,6 @@ func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSnapshotStatus(id uint, status string, message string) {
|
||||
if status != constant.StatusSuccess {
|
||||
global.LOG.Errorf("snapshot failed, err: %s", message)
|
||||
}
|
||||
if err := snapshotRepo.Update(id, map[string]interface{}{
|
||||
"status": status,
|
||||
"message": message,
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("update snap snapshot status failed, err: %v", err)
|
||||
}
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
}
|
||||
func updateRecoverStatus(id uint, interruptStep, status string, message string) {
|
||||
if status != constant.StatusSuccess {
|
||||
global.LOG.Errorf("recover failed, err: %s", message)
|
||||
@ -939,35 +905,6 @@ func (u *SnapshotService) updateLiveRestore(enabled bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) handleTar(sourceDir, targetDir, name, exclusionRules string) error {
|
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exStr := ""
|
||||
excludes := strings.Split(exclusionRules, ";")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
}
|
||||
exStr += " --exclude "
|
||||
exStr += exclude
|
||||
}
|
||||
|
||||
commands := fmt.Sprintf("tar --warning=no-file-changed -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 (u *SnapshotService) handleUnTar(sourceDir, targetDir string) error {
|
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
|
||||
@ -1035,3 +972,35 @@ func min(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func checkIsAllDone(snapID uint) bool {
|
||||
status, err := snapshotRepo.GetStatus(snapID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if status.Panel != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.PanelCtl != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.PanelService != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.PanelInfo != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.DaemonJson != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.AppData != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.PanelData != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
if status.BackupData != constant.StatusDone {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
224
backend/app/service/snapshot_create.go
Normal file
224
backend/app/service/snapshot_create.go
Normal file
@ -0,0 +1,224 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
)
|
||||
|
||||
type snapHelper struct {
|
||||
SnapID uint
|
||||
Ctx context.Context
|
||||
FileOp files.FileOp
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func snapJson(snap snapHelper, statusID uint, snapJson SnapshotJson, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": constant.Running})
|
||||
status := constant.StatusDone
|
||||
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t")
|
||||
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": status})
|
||||
}
|
||||
|
||||
func snapPanel(snap snapHelper, statusID uint, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := cpBinary("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": status})
|
||||
}
|
||||
|
||||
func snapPanelCtl(snap snapHelper, statusID uint, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := cpBinary("/usr/local/bin/1pctl", path.Join(targetDir, "1pctl")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": status})
|
||||
}
|
||||
|
||||
func snapPanelService(snap snapHelper, statusID uint, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := cpBinary("/etc/systemd/system/1panel.service", path.Join(targetDir, "1panel.service")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": status})
|
||||
}
|
||||
|
||||
func snapDaemonJson(snap snapHelper, statusID uint, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
if !snap.FileOp.Stat("/etc/docker/daemon.json") {
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := cpBinary("/etc/docker/daemon.json", path.Join(targetDir, "daemon.json")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": status})
|
||||
}
|
||||
|
||||
func snapAppData(snap snapHelper, statusID uint, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.Running})
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
if err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
}
|
||||
runtimes, err := runtimeRepo.List()
|
||||
if err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
}
|
||||
imageRegex := regexp.MustCompile(`image:\s*(.*)`)
|
||||
var imageSaveList []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 _, rumtime := range runtimes {
|
||||
for _, existImage := range existImages {
|
||||
if rumtime.Image == existImage && !duplicateMap[rumtime.Image] {
|
||||
imageSaveList = append(imageSaveList, rumtime.Image)
|
||||
duplicateMap[rumtime.Image] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
|
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
|
||||
if err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": std})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.StatusDone})
|
||||
}
|
||||
|
||||
func snapBackup(snap snapHelper, statusID uint, localDir, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;"); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": status})
|
||||
}
|
||||
|
||||
func snapPanelData(snap snapHelper, statusID uint, localDir, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, 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(localDir, dataDir) {
|
||||
exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";")
|
||||
}
|
||||
|
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"})
|
||||
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})
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_data": status})
|
||||
}
|
||||
|
||||
func snapCompress(snap snapHelper, statusID uint, rootDir string) {
|
||||
defer func() {
|
||||
global.LOG.Debugf("remove snapshot file %s", rootDir)
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}()
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusRunning})
|
||||
tmpDir := path.Join(global.CONF.System.TmpDir, "system")
|
||||
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir))
|
||||
if err := snap.FileOp.Compress([]string{rootDir}, tmpDir, fileName, files.TarGz); err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusDone})
|
||||
}
|
||||
|
||||
func snapUpload(account string, statusID uint, file string) {
|
||||
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file))
|
||||
defer func() {
|
||||
global.LOG.Debugf("remove snapshot file %s", source)
|
||||
_ = os.Remove(source)
|
||||
}()
|
||||
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusUploading})
|
||||
backup, err := backupRepo.Get(commonRepo.WithByType(account))
|
||||
if err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()})
|
||||
return
|
||||
}
|
||||
client, err := NewIBackupService().NewClient(&backup)
|
||||
if err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()})
|
||||
return
|
||||
}
|
||||
target := path.Join(backup.BackupPath, "system_snapshot", path.Base(file))
|
||||
if _, err := client.Upload(source, target); err != nil {
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusDone})
|
||||
}
|
||||
|
||||
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
|
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exStr := ""
|
||||
excludes := strings.Split(exclusionRules, ";")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
}
|
||||
exStr += " --exclude "
|
||||
exStr += exclude
|
||||
}
|
||||
|
||||
commands := fmt.Sprintf("tar --warning=no-file-changed -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
|
||||
}
|
@ -2,6 +2,7 @@ package constant
|
||||
|
||||
const (
|
||||
StatusRunning = "Running"
|
||||
StatusDone = "Done"
|
||||
StatusStoped = "Stoped"
|
||||
StatusWaiting = "Waiting"
|
||||
StatusSuccess = "Success"
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
@ -63,4 +64,57 @@ func Init() {
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
_, _ = cmd.Execf("%s sed -i '/CHANGE_USER_INFO=true/d' /usr/local/bin/1pctl", sudo)
|
||||
}
|
||||
|
||||
handleSnapStatus()
|
||||
}
|
||||
|
||||
func handleSnapStatus() {
|
||||
snapRepo := repo.NewISnapshotRepo()
|
||||
snaps, _ := snapRepo.GetList()
|
||||
for _, snap := range snaps {
|
||||
if snap.Status == "OnSaveData" {
|
||||
_ = snapRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}
|
||||
if snap.Status == constant.StatusWaiting {
|
||||
_ = snapRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": "the task was interrupted due to the restart of the 1panel service"})
|
||||
}
|
||||
}
|
||||
|
||||
status, _ := snapRepo.GetStatusList()
|
||||
for _, statu := range status {
|
||||
updatas := make(map[string]interface{})
|
||||
if statu.Panel == constant.StatusRunning {
|
||||
updatas["panel"] = constant.StatusFailed
|
||||
}
|
||||
if statu.PanelCtl == constant.StatusRunning {
|
||||
updatas["panel_ctl"] = constant.StatusFailed
|
||||
}
|
||||
if statu.PanelService == constant.StatusRunning {
|
||||
updatas["panel_service"] = constant.StatusFailed
|
||||
}
|
||||
if statu.PanelInfo == constant.StatusRunning {
|
||||
updatas["panel_info"] = constant.StatusFailed
|
||||
}
|
||||
if statu.DaemonJson == constant.StatusRunning {
|
||||
updatas["daemon_json"] = constant.StatusFailed
|
||||
}
|
||||
if statu.AppData == constant.StatusRunning {
|
||||
updatas["app_data"] = constant.StatusFailed
|
||||
}
|
||||
if statu.PanelData == constant.StatusRunning {
|
||||
updatas["panel_data"] = constant.StatusFailed
|
||||
}
|
||||
if statu.BackupData == constant.StatusRunning {
|
||||
updatas["backup_data"] = constant.StatusFailed
|
||||
}
|
||||
if statu.Compress == constant.StatusRunning {
|
||||
updatas["compress"] = constant.StatusFailed
|
||||
}
|
||||
if statu.Upload == constant.StatusUploading {
|
||||
updatas["upload"] = constant.StatusFailed
|
||||
}
|
||||
if len(updatas) != 0 {
|
||||
_ = snapRepo.UpdateStatus(statu.ID, updatas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -572,9 +572,9 @@ var UpdateCronjobWithDb = &gormigrate.Migration{
|
||||
}
|
||||
|
||||
var AddTableFirewall = &gormigrate.Migration{
|
||||
ID: "20230814-add-table-firewall",
|
||||
ID: "20230821-add-table-firewall",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.Firewall{}); err != nil {
|
||||
if err := tx.AutoMigrate(&model.Firewall{}, model.SnapshotStatus{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -34,6 +34,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
|
||||
settingRouter.POST("/mfa/bind", baseApi.MFABind)
|
||||
|
||||
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||
settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus)
|
||||
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
|
||||
settingRouter.POST("/snapshot/import", baseApi.ImportSnapshot)
|
||||
settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot)
|
||||
|
@ -76,6 +76,7 @@ export namespace Setting {
|
||||
interval: string;
|
||||
}
|
||||
export interface SnapshotCreate {
|
||||
id: number;
|
||||
from: string;
|
||||
description: string;
|
||||
}
|
||||
@ -106,6 +107,19 @@ export namespace Setting {
|
||||
rollbackMessage: string;
|
||||
lastRollbackedAt: string;
|
||||
}
|
||||
export interface SnapshotStatus {
|
||||
panel: string;
|
||||
panelCtl: string;
|
||||
panelService: string;
|
||||
panelInfo: string;
|
||||
daemonJson: string;
|
||||
appData: string;
|
||||
panelData: string;
|
||||
backupData: string;
|
||||
|
||||
compress: string;
|
||||
upload: string;
|
||||
}
|
||||
export interface UpgradeInfo {
|
||||
newVersion: string;
|
||||
latestVersion: string;
|
||||
|
@ -132,6 +132,9 @@ export const listBucket = (params: Backup.ForBucket) => {
|
||||
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
|
||||
return http.post(`/settings/snapshot`, param);
|
||||
};
|
||||
export const loadSnapStatus = (id: number) => {
|
||||
return http.post<Setting.SnapshotStatus>(`/settings/snapshot/status`, { id: id });
|
||||
};
|
||||
export const snapshotImport = (param: Setting.SnapshotImport) => {
|
||||
return http.post(`/settings/snapshot/import`, param);
|
||||
};
|
||||
|
@ -198,8 +198,10 @@ const message = {
|
||||
},
|
||||
status: {
|
||||
running: 'Running',
|
||||
done: 'Done',
|
||||
success: 'Success',
|
||||
waiting: 'Waiting',
|
||||
waiting1: 'Waiting',
|
||||
failed: 'Failed',
|
||||
stopped: 'Stopped',
|
||||
error: 'Error',
|
||||
@ -1106,6 +1108,17 @@ const message = {
|
||||
certificate: 'Certificate',
|
||||
|
||||
snapshot: 'Snapshot',
|
||||
status: 'Snapshot status',
|
||||
panelBin: 'Backup 1Panel binary',
|
||||
panelCtl: 'Backup 1Panel script',
|
||||
panelService: 'Backup 1Panel service',
|
||||
panelInfo: 'Backup 1Panel basic information',
|
||||
daemonJson: 'Backup Docker daemon.json',
|
||||
appData: 'Backup 1Panel application',
|
||||
panelData: 'Backup 1Panel data directory',
|
||||
backupData: 'Backup 1Panel local backup directory',
|
||||
compress: 'Compress snapshot file',
|
||||
upload: 'Upload snapshot file',
|
||||
thirdPartySupport: 'Only third-party accounts are supported',
|
||||
recoverDetail: 'Recover detail',
|
||||
createSnapshot: 'Create snapshot',
|
||||
|
@ -196,8 +196,10 @@ const message = {
|
||||
},
|
||||
status: {
|
||||
running: '已啟動',
|
||||
done: '已完成',
|
||||
success: '成功',
|
||||
waiting: '執行中',
|
||||
waiting1: '等待中',
|
||||
failed: '失敗',
|
||||
stopped: '已停止',
|
||||
error: '失敗',
|
||||
@ -997,6 +999,17 @@ const message = {
|
||||
path: '路徑',
|
||||
|
||||
snapshot: '快照',
|
||||
status: '快照狀態',
|
||||
panelBin: '備份 1Panel 二進製',
|
||||
panelCtl: '備份 1Panel 腳本',
|
||||
panelService: '備份 1Panel 服務',
|
||||
panelInfo: '備份 1Panel 基礎信息',
|
||||
daemonJson: '備份 Docker 配置',
|
||||
appData: '備份 1Panel 應用',
|
||||
panelData: '備份 1Panel 數據目錄',
|
||||
backupData: '備份 1Panel 本地備份目錄',
|
||||
compress: '壓縮快照文件',
|
||||
upload: '上傳快照文件',
|
||||
thirdPartySupport: '僅支持第三方賬號',
|
||||
recoverDetail: '恢復詳情',
|
||||
createSnapshot: '創建快照',
|
||||
|
@ -196,8 +196,10 @@ const message = {
|
||||
},
|
||||
status: {
|
||||
running: '已启动',
|
||||
done: '已完成',
|
||||
success: '成功',
|
||||
waiting: '执行中',
|
||||
waiting1: '等待中',
|
||||
failed: '失败',
|
||||
stopped: '已停止',
|
||||
error: '失败',
|
||||
@ -997,6 +999,17 @@ const message = {
|
||||
path: '路径',
|
||||
|
||||
snapshot: '快照',
|
||||
status: '快照状态',
|
||||
panelBin: '备份 1Panel 二进制',
|
||||
panelCtl: '备份 1Panel 脚本',
|
||||
panelService: '备份 1Panel 服务',
|
||||
panelInfo: '备份 1Panel 基础信息',
|
||||
daemonJson: '备份 Docker 配置',
|
||||
appData: '备份 1Panel 应用',
|
||||
panelData: '备份 1Panel 数据目录',
|
||||
backupData: '备份 1Panel 本地备份目录',
|
||||
compress: '压缩快照文件',
|
||||
upload: '上传快照文件',
|
||||
thirdPartySupport: '仅支持第三方账号',
|
||||
recoverDetail: '恢复详情',
|
||||
createSnapshot: '创建快照',
|
||||
|
@ -56,21 +56,20 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'Success'" type="success">
|
||||
{{ $t('commons.table.statusSuccess') }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.status === 'Waiting'" type="info">
|
||||
<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>
|
||||
<el-tag v-if="row.status === 'Uploading'" type="info">
|
||||
{{ $t('commons.status.uploading') }}...
|
||||
</el-tag>
|
||||
<el-tooltip v-if="row.status === 'Failed'" effect="dark" placement="top">
|
||||
<template #content>
|
||||
<div style="width: 300px; word-break: break-all">{{ row.message }}</div>
|
||||
</template>
|
||||
<el-tag type="danger">{{ $t('commons.table.statusFailed') }}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.description')" prop="description">
|
||||
@ -142,6 +141,7 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<SnapStatus ref="snapStatusRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -156,6 +156,7 @@ import { ElForm } from 'element-plus';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { Setting } from '@/api/interface/setting';
|
||||
import SnapStatus from '@/views/setting/snapshot/snap_status/index.vue';
|
||||
import RecoverStatus from '@/views/setting/snapshot/status/index.vue';
|
||||
import SnapshotImport from '@/views/setting/snapshot/import/index.vue';
|
||||
import { getBackupList } from '@/api/modules/setting';
|
||||
@ -171,6 +172,7 @@ const paginationConfig = reactive({
|
||||
});
|
||||
const searchName = ref();
|
||||
|
||||
const snapStatusRef = ref();
|
||||
const recoverStatusRef = ref();
|
||||
const importRef = ref();
|
||||
const isRecordShow = ref();
|
||||
@ -182,6 +184,7 @@ const rules = reactive({
|
||||
});
|
||||
|
||||
let snapInfo = reactive<Setting.SnapshotCreate>({
|
||||
id: 0,
|
||||
from: '',
|
||||
description: '',
|
||||
});
|
||||
@ -226,6 +229,10 @@ const submitAddSnapshot = (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadStatus = (row: Setting.SnapshotInfo) => {
|
||||
snapStatusRef.value.acceptParams({ id: row.id, from: row.from, description: row.description });
|
||||
};
|
||||
|
||||
const loadBackups = async () => {
|
||||
const res = await getBackupList();
|
||||
backupOptions.value = [];
|
||||
|
338
frontend/src/views/setting/snapshot/snap_status/index.vue
Normal file
338
frontend/src/views/setting/snapshot/snap_status/index.vue
Normal file
@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisiable"
|
||||
@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.panel)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panel)" link>{{ $t('setting.panelBin') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panel)" class="top-margin">
|
||||
<span class="err-message">{{ status.panel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.panelCtl)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panelCtl)" link>{{ $t('setting.panelCtl') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panelCtl)" class="top-margin">
|
||||
<span class="err-message">{{ status.panelCtl }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.panelService)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panelService)" link>{{ $t('setting.panelService') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panelService)" class="top-margin">
|
||||
<span class="err-message">{{ status.panelService }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.panelInfo)" :closable="false">
|
||||
<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.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.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.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.compress)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.compress)" link>{{ $t('setting.compress') }}</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: '',
|
||||
panelCtl: '',
|
||||
panelService: '',
|
||||
panelInfo: '',
|
||||
daemonJson: '',
|
||||
appData: '',
|
||||
panelData: '',
|
||||
backupData: '',
|
||||
|
||||
compress: '',
|
||||
upload: '',
|
||||
});
|
||||
|
||||
const dialogVisiable = ref(false);
|
||||
|
||||
const loading = ref();
|
||||
const snapID = ref();
|
||||
const snapFrom = ref();
|
||||
const snapDescription = ref();
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
interface DialogProps {
|
||||
id: number;
|
||||
from: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
dialogVisiable.value = true;
|
||||
snapID.value = props.id;
|
||||
snapFrom.value = props.from;
|
||||
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.panelCtl = res.data.panelCtl;
|
||||
status.panelService = res.data.panelService;
|
||||
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.upload = res.data.upload;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
emit('search');
|
||||
dialogVisiable.value = false;
|
||||
};
|
||||
|
||||
const onRetry = async () => {
|
||||
loading.value = true;
|
||||
await snapshotCreate({ id: snapID.value, from: snapFrom.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.panelCtl = res.data.panelCtl;
|
||||
status.panelService = res.data.panelService;
|
||||
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.upload = res.data.upload;
|
||||
}
|
||||
}, 1000 * 3);
|
||||
};
|
||||
|
||||
const keepLoadStatus = () => {
|
||||
if (status.panel === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelCtl === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelService === '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.panelCtl !== 'Running' && status.panelCtl !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelService !== 'Running' && status.panelService !== '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…
x
Reference in New Issue
Block a user