mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-09-20 07:35:58 +08:00
feat: 优化快照功能
This commit is contained in:
parent
53cfb2e755
commit
cac60477eb
|
@ -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 创建系统快照
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type SettingInfo struct {
|
||||
SystemIP string `json:"systemIP"`
|
||||
DockerSockPath string `json:"dockerSockPath"`
|
||||
|
@ -34,64 +32,6 @@ type SettingUpdate struct {
|
|||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type SnapshotStatus struct {
|
||||
Panel string `json:"panel"`
|
||||
PanelInfo string `json:"panelInfo"`
|
||||
DaemonJson string `json:"daemonJson"`
|
||||
AppData string `json:"appData"`
|
||||
PanelData string `json:"panelData"`
|
||||
BackupData string `json:"backupData"`
|
||||
|
||||
Compress string `json:"compress"`
|
||||
Size string `json:"size"`
|
||||
Upload string `json:"upload"`
|
||||
}
|
||||
|
||||
type SnapshotCreate struct {
|
||||
ID uint `json:"id"`
|
||||
SourceAccountIDs string `json:"sourceAccountIDs" validate:"required"`
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
type SnapshotRecover struct {
|
||||
IsNew bool `json:"isNew"`
|
||||
ReDownload bool `json:"reDownload"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
type SnapshotBatchDelete struct {
|
||||
DeleteWithFile bool `json:"deleteWithFile"`
|
||||
Ids []uint `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type SnapshotImport struct {
|
||||
BackupAccountID uint `json:"backupAccountID"`
|
||||
Names []string `json:"names"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
}
|
||||
|
||||
type SnapshotInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
From string `json:"from"`
|
||||
DefaultDownload string `json:"defaultDownload"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"`
|
||||
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
RecoverStatus string `json:"recoverStatus"`
|
||||
RecoverMessage string `json:"recoverMessage"`
|
||||
LastRecoveredAt string `json:"lastRecoveredAt"`
|
||||
RollbackStatus string `json:"rollbackStatus"`
|
||||
RollbackMessage string `json:"rollbackMessage"`
|
||||
LastRollbackedAt string `json:"lastRollbackedAt"`
|
||||
}
|
||||
|
||||
type SyncTime struct {
|
||||
NtpSite string `json:"ntpSite" validate:"required"`
|
||||
}
|
||||
|
|
90
agent/app/dto/snapshot.go
Normal file
90
agent/app/dto/snapshot.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type SnapshotStatus struct {
|
||||
BaseData string `json:"baseData"`
|
||||
AppImage string `json:"appImage"`
|
||||
PanelData string `json:"panelData"`
|
||||
BackupData string `json:"backupData"`
|
||||
|
||||
Compress string `json:"compress"`
|
||||
Size string `json:"size"`
|
||||
Upload string `json:"upload"`
|
||||
}
|
||||
|
||||
type SnapshotCreate struct {
|
||||
ID uint `json:"id"`
|
||||
SourceAccountIDs string `json:"sourceAccountIDs" validate:"required"`
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
AppData []DataTree `json:"appData"`
|
||||
BackupData []DataTree `json:"backupData"`
|
||||
PanelData []DataTree `json:"panelData"`
|
||||
|
||||
WithMonitorData bool `json:"withMonitorData"`
|
||||
WithLoginLog bool `json:"withLoginLog"`
|
||||
WithOperationLog bool `json:"withOperationLog"`
|
||||
}
|
||||
|
||||
type SnapshotData struct {
|
||||
AppData []DataTree `json:"appData"`
|
||||
BackupData []DataTree `json:"backupData"`
|
||||
PanelData []DataTree `json:"panelData"`
|
||||
|
||||
WithMonitorData bool `json:"withMonitorData"`
|
||||
WithLoginLog bool `json:"withLoginLog"`
|
||||
WithOperationLog bool `json:"withOperationLog"`
|
||||
}
|
||||
type DataTree struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
IsCheck bool `json:"isCheck"`
|
||||
IsDisable bool `json:"isDisable"`
|
||||
|
||||
Path string `json:"path"`
|
||||
|
||||
Children []DataTree `json:"children"`
|
||||
}
|
||||
type SnapshotRecover struct {
|
||||
IsNew bool `json:"isNew"`
|
||||
ReDownload bool `json:"reDownload"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
type SnapshotBatchDelete struct {
|
||||
DeleteWithFile bool `json:"deleteWithFile"`
|
||||
Ids []uint `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type SnapshotImport struct {
|
||||
BackupAccountID uint `json:"backupAccountID"`
|
||||
Names []string `json:"names"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
}
|
||||
|
||||
type SnapshotInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
From string `json:"from"`
|
||||
DefaultDownload string `json:"defaultDownload"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"`
|
||||
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
RecoverStatus string `json:"recoverStatus"`
|
||||
RecoverMessage string `json:"recoverMessage"`
|
||||
LastRecoveredAt string `json:"lastRecoveredAt"`
|
||||
RollbackStatus string `json:"rollbackStatus"`
|
||||
RollbackMessage string `json:"rollbackMessage"`
|
||||
LastRollbackedAt string `json:"lastRollbackedAt"`
|
||||
}
|
|
@ -10,6 +10,13 @@ type Snapshot struct {
|
|||
Message string `json:"message"`
|
||||
Version string `json:"version"`
|
||||
|
||||
AppData string `json:"appData"`
|
||||
PanelData string `json:"panelData"`
|
||||
BackupData string `json:"backupData"`
|
||||
WithMonitorData bool `json:"withMonitorData"`
|
||||
WithLoginLog bool `json:"withLoginLog"`
|
||||
WithOperationLog bool `json:"withOperationLog"`
|
||||
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
RecoverStatus string `json:"recoverStatus"`
|
||||
RecoverMessage string `json:"recoverMessage"`
|
||||
|
@ -21,11 +28,10 @@ type Snapshot struct {
|
|||
|
||||
type SnapshotStatus struct {
|
||||
BaseModel
|
||||
SnapID uint `json:"snapID"`
|
||||
Panel string `json:"panel" gorm:"default:Running"`
|
||||
PanelInfo string `json:"panelInfo" gorm:"default:Running"`
|
||||
DaemonJson string `json:"daemonJson" gorm:"default:Running"`
|
||||
AppData string `json:"appData" gorm:"default:Running"`
|
||||
SnapID uint `json:"snapID"`
|
||||
|
||||
BaseData string `json:"baseData" gorm:"default:Running"`
|
||||
AppImage string `json:"appImage" gorm:"default:Running"`
|
||||
PanelData string `json:"panelData" gorm:"default:Running"`
|
||||
BackupData string `json:"backupData" gorm:"default:Running"`
|
||||
|
||||
|
|
|
@ -16,7 +16,11 @@ import (
|
|||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
|
@ -28,6 +32,7 @@ type SnapshotService struct {
|
|||
|
||||
type ISnapshotService interface {
|
||||
SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
LoadSnapshotData() (dto.SnapshotData, error)
|
||||
SnapshotCreate(req dto.SnapshotCreate) error
|
||||
SnapshotRecover(req dto.SnapshotRecover) error
|
||||
SnapshotRollback(req dto.SnapshotRecover) error
|
||||
|
@ -92,6 +97,37 @@ func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) LoadSnapshotData() (dto.SnapshotData, error) {
|
||||
var (
|
||||
data dto.SnapshotData
|
||||
err error
|
||||
)
|
||||
fileOp := fileUtils.NewFileOp()
|
||||
data.AppData, err = loadApps(fileOp)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.PanelData, err = loadPanelFile(fileOp)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
itemBackups, err := loadFile(global.CONF.System.Backup, 8, fileOp)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
for i, item := range itemBackups {
|
||||
if item.Label == "app" {
|
||||
data.BackupData = append(itemBackups[:i], itemBackups[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
data.WithLoginLog = true
|
||||
data.WithOperationLog = true
|
||||
data.WithMonitorData = false
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
@ -109,16 +145,9 @@ func (u *SnapshotService) LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, erro
|
|||
}
|
||||
|
||||
type SnapshotJson struct {
|
||||
OldBaseDir string `json:"oldBaseDir"`
|
||||
OldDockerDataDir string `json:"oldDockerDataDir"`
|
||||
OldBackupDataDir string `json:"oldBackupDataDir"`
|
||||
OldPanelDataDir string `json:"oldPanelDataDir"`
|
||||
|
||||
BaseDir string `json:"baseDir"`
|
||||
DockerDataDir string `json:"dockerDataDir"`
|
||||
BackupDataDir string `json:"backupDataDir"`
|
||||
PanelDataDir string `json:"panelDataDir"`
|
||||
LiveRestoreEnabled bool `json:"liveRestoreEnabled"`
|
||||
BaseDir string `json:"baseDir"`
|
||||
BackupDataDir string `json:"backupDataDir"`
|
||||
Size uint64 `json:"size"`
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
|
||||
|
@ -148,7 +177,7 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
|
|||
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
|
||||
_ = settingRepo.Update("SystemStatus", "Recovering")
|
||||
go u.HandleSnapshotRecover(snap, true, req)
|
||||
go u.HandleSnapshotRecover(snap, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -158,9 +187,11 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.IsNew = false
|
||||
snap.InterruptStep = "Readjson"
|
||||
go u.HandleSnapshotRecover(snap, false, req)
|
||||
go func() {
|
||||
if err := handleRollback(snap.Name); err != nil {
|
||||
global.LOG.Errorf("handle roll back snapshot failed, err: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -194,15 +225,26 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
|
|||
if isCronjob {
|
||||
name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
|
||||
}
|
||||
rootDir = path.Join(global.CONF.System.Backup, "system", name)
|
||||
rootDir = path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", name)
|
||||
|
||||
appItem, _ := json.Marshal(req.AppData)
|
||||
panelItem, _ := json.Marshal(req.PanelData)
|
||||
backupItem, _ := json.Marshal(req.BackupData)
|
||||
snap = model.Snapshot{
|
||||
Name: name,
|
||||
Description: req.Description,
|
||||
SourceAccountIDs: req.SourceAccountIDs,
|
||||
DownloadAccountID: req.DownloadAccountID,
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusWaiting,
|
||||
|
||||
AppData: string(appItem),
|
||||
PanelData: string(panelItem),
|
||||
BackupData: string(backupItem),
|
||||
WithMonitorData: req.WithMonitorData,
|
||||
WithLoginLog: req.WithLoginLog,
|
||||
WithOperationLog: req.WithOperationLog,
|
||||
|
||||
Version: versionItem.Value,
|
||||
Status: constant.StatusWaiting,
|
||||
}
|
||||
_ = snapshotRepo.Create(&snap)
|
||||
snapStatus.SnapID = snap.ID
|
||||
|
@ -218,57 +260,43 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
|
|||
snapStatus.SnapID = snap.ID
|
||||
_ = snapshotRepo.CreateStatus(&snapStatus)
|
||||
}
|
||||
rootDir = path.Join(global.CONF.System.Backup, fmt.Sprintf("system/%s", snap.Name))
|
||||
rootDir = path.Join(global.CONF.System.BaseDir, "1panel/tmp/system", snap.Name)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
itemHelper := snapHelper{SnapID: snap.ID, Status: &snapStatus, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
|
||||
backupPanelDir := path.Join(rootDir, "1panel")
|
||||
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
|
||||
backupDockerDir := path.Join(rootDir, "docker")
|
||||
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
|
||||
baseDir := path.Join(rootDir, "base")
|
||||
_ = os.MkdirAll(baseDir, os.ModePerm)
|
||||
if err := loadDbConn(&itemHelper, rootDir, req); err != nil {
|
||||
return "", fmt.Errorf("load snapshot db conn failed, err: %v", err)
|
||||
}
|
||||
|
||||
jsonItem := SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: global.CONF.System.Backup,
|
||||
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
|
||||
}
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
if snapStatus.PanelInfo != constant.StatusDone {
|
||||
if snapStatus.BaseData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapJson(itemHelper, jsonItem, rootDir)
|
||||
go snapBaseData(itemHelper, baseDir)
|
||||
}
|
||||
if snapStatus.Panel != constant.StatusDone {
|
||||
if snapStatus.AppImage != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanel(itemHelper, backupPanelDir)
|
||||
}
|
||||
if snapStatus.DaemonJson != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapDaemonJson(itemHelper, backupDockerDir)
|
||||
}
|
||||
if snapStatus.AppData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapAppData(itemHelper, backupDockerDir)
|
||||
go snapAppImage(itemHelper, req, rootDir)
|
||||
}
|
||||
if snapStatus.BackupData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapBackup(itemHelper, backupPanelDir)
|
||||
go snapBackupData(itemHelper, req, rootDir)
|
||||
}
|
||||
if snapStatus.PanelData != constant.StatusDone {
|
||||
wg.Add(1)
|
||||
go snapPanelData(itemHelper, req, rootDir)
|
||||
}
|
||||
|
||||
if !isCronjob {
|
||||
go func() {
|
||||
wg.Wait()
|
||||
closeDatabase(itemHelper.snapAgentDB)
|
||||
closeDatabase(itemHelper.snapCoreDB)
|
||||
if !checkIsAllDone(snap.ID) {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
return
|
||||
}
|
||||
if snapStatus.PanelData != constant.StatusDone {
|
||||
snapPanelData(itemHelper, backupPanelDir)
|
||||
}
|
||||
if snapStatus.PanelData != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
return
|
||||
}
|
||||
if snapStatus.Compress != constant.StatusDone {
|
||||
snapCompress(itemHelper, rootDir, secret)
|
||||
}
|
||||
|
@ -284,23 +312,20 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
|
|||
return
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
// _ = snapshotRepo.DeleteStatus(itemHelper.SnapID)
|
||||
_ = os.RemoveAll(rootDir)
|
||||
}()
|
||||
return "", nil
|
||||
}
|
||||
wg.Wait()
|
||||
closeDatabase(itemHelper.snapAgentDB)
|
||||
closeDatabase(itemHelper.snapCoreDB)
|
||||
if !checkIsAllDone(snap.ID) {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
return snap.Name, fmt.Errorf("snapshot %s backup failed", snap.Name)
|
||||
}
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
snapPanelData(itemHelper, backupPanelDir)
|
||||
if snapStatus.PanelData != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
return snap.Name, fmt.Errorf("snapshot %s 1panel data failed", snap.Name)
|
||||
}
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
snapCompress(itemHelper, rootDir, secret)
|
||||
if snapStatus.Compress != constant.StatusDone {
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
|
||||
|
@ -316,6 +341,7 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
|
|||
}
|
||||
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
loadLogByStatus(snapStatus, logPath)
|
||||
_ = os.RemoveAll(rootDir)
|
||||
return snap.Name, nil
|
||||
}
|
||||
|
||||
|
@ -341,71 +367,6 @@ func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func updateRecoverStatus(id uint, isRecover bool, interruptStep, status, message string) {
|
||||
if isRecover {
|
||||
if status != constant.StatusSuccess {
|
||||
global.LOG.Errorf("recover failed, err: %s", message)
|
||||
}
|
||||
if err := snapshotRepo.Update(id, map[string]interface{}{
|
||||
"interrupt_step": interruptStep,
|
||||
"recover_status": status,
|
||||
"recover_message": message,
|
||||
"last_recovered_at": time.Now().Format(constant.DateTimeLayout),
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("update snap recover status failed, err: %v", err)
|
||||
}
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
return
|
||||
}
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
if status == constant.StatusSuccess {
|
||||
if err := snapshotRepo.Update(id, map[string]interface{}{
|
||||
"recover_status": "",
|
||||
"recover_message": "",
|
||||
"interrupt_step": "",
|
||||
"rollback_status": "",
|
||||
"rollback_message": "",
|
||||
"last_rollbacked_at": time.Now().Format(constant.DateTimeLayout),
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("update snap recover status failed, err: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
global.LOG.Errorf("rollback failed, err: %s", message)
|
||||
if err := snapshotRepo.Update(id, map[string]interface{}{
|
||||
"rollback_status": status,
|
||||
"rollback_message": message,
|
||||
"last_rollbacked_at": time.Now().Format(constant.DateTimeLayout),
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("update snap recover status failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *SnapshotService) handleUnTar(sourceDir, targetDir string, secret string) error {
|
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
commands := ""
|
||||
if len(secret) != 0 {
|
||||
extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceDir + " | "
|
||||
commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1")
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar zxvfC %s %s", sourceDir, targetDir)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
|
||||
if err != nil {
|
||||
if len(stdout) != 0 {
|
||||
global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err)
|
||||
return fmt.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rebuildAllAppInstall() error {
|
||||
global.LOG.Debug("start to rebuild all app")
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
|
@ -449,17 +410,14 @@ func checkIsAllDone(snapID uint) bool {
|
|||
}
|
||||
|
||||
func checkAllDone(status model.SnapshotStatus) (bool, string) {
|
||||
if status.Panel != constant.StatusDone {
|
||||
return false, status.Panel
|
||||
if status.BaseData != constant.StatusDone {
|
||||
return false, status.BaseData
|
||||
}
|
||||
if status.PanelInfo != constant.StatusDone {
|
||||
return false, status.PanelInfo
|
||||
if status.PanelData != constant.StatusDone {
|
||||
return false, status.PanelData
|
||||
}
|
||||
if status.DaemonJson != constant.StatusDone {
|
||||
return false, status.DaemonJson
|
||||
}
|
||||
if status.AppData != constant.StatusDone {
|
||||
return false, status.AppData
|
||||
if status.AppImage != constant.StatusDone {
|
||||
return false, status.AppImage
|
||||
}
|
||||
if status.BackupData != constant.StatusDone {
|
||||
return false, status.BackupData
|
||||
|
@ -469,10 +427,8 @@ func checkAllDone(status model.SnapshotStatus) (bool, string) {
|
|||
|
||||
func loadLogByStatus(status model.SnapshotStatus, logPath string) {
|
||||
logs := ""
|
||||
logs += fmt.Sprintf("Write 1Panel basic information: %s \n", status.PanelInfo)
|
||||
logs += fmt.Sprintf("Backup 1Panel system files: %s \n", status.Panel)
|
||||
logs += fmt.Sprintf("Backup Docker configuration file: %s \n", status.DaemonJson)
|
||||
logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppData)
|
||||
logs += fmt.Sprintf("Backup 1Panel base files: %s \n", status.BaseData)
|
||||
logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppImage)
|
||||
logs += fmt.Sprintf("Backup 1Panel data directory: %s \n", status.PanelData)
|
||||
logs += fmt.Sprintf("Backup local backup directory for 1Panel: %s \n", status.BackupData)
|
||||
logs += fmt.Sprintf("Create snapshot file: %s \n", status.Compress)
|
||||
|
@ -544,3 +500,154 @@ func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotInfo, error) {
|
|||
wg.Wait()
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
func loadApps(fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
|
||||
var data []dto.DataTree
|
||||
apps, err := appInstallRepo.ListBy()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
client, err := docker.NewDockerClient()
|
||||
hasDockerClient := true
|
||||
if err != nil {
|
||||
hasDockerClient = false
|
||||
global.LOG.Errorf("new docker client failed, err: %v", err)
|
||||
} else {
|
||||
defer client.Close()
|
||||
}
|
||||
imageList, err := client.ImageList(context.Background(), image.ListOptions{})
|
||||
if err != nil {
|
||||
hasDockerClient = false
|
||||
global.LOG.Errorf("load image list failed, err: %v", err)
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
itemApp := dto.DataTree{ID: uuid.NewString(), Label: fmt.Sprintf("%s - %s", app.App.Name, app.Name), Key: app.App.Key, Name: app.Name}
|
||||
appPath := path.Join(global.CONF.System.BaseDir, "1panel/apps", app.App.Key, app.Name)
|
||||
itemAppData := dto.DataTree{ID: uuid.NewString(), Label: "appData", Key: app.App.Key, Name: app.Name, IsCheck: true, Path: appPath}
|
||||
sizeItem, err := fileOp.GetDirSize(appPath)
|
||||
if err == nil {
|
||||
itemAppData.Size = uint64(sizeItem)
|
||||
}
|
||||
itemApp.Size += itemAppData.Size
|
||||
itemApp.Children = append(itemApp.Children, itemAppData)
|
||||
|
||||
appBackupPath := path.Join(global.CONF.System.BaseDir, "1panel/backup/app", app.App.Key, app.Name)
|
||||
itemAppBackupTree, err := loadFile(appBackupPath, 8, fileOp)
|
||||
itemAppBackup := dto.DataTree{ID: uuid.NewString(), Label: "appBackup", IsCheck: true, Children: itemAppBackupTree, Path: appBackupPath}
|
||||
if err == nil {
|
||||
backupSizeItem, err := fileOp.GetDirSize(appBackupPath)
|
||||
if err == nil {
|
||||
itemAppBackup.Size = uint64(backupSizeItem)
|
||||
itemApp.Size += itemAppBackup.Size
|
||||
}
|
||||
itemApp.Children = append(itemApp.Children, itemAppBackup)
|
||||
}
|
||||
|
||||
itemAppImage := dto.DataTree{ID: uuid.NewString(), Label: "appImage"}
|
||||
stdout, err := cmd.Execf("cat %s | grep image: ", path.Join(global.CONF.System.BaseDir, "1panel/apps", app.App.Key, app.Name, "docker-compose.yml"))
|
||||
if err != nil {
|
||||
itemApp.Children = append(itemApp.Children, itemAppImage)
|
||||
data = append(data, itemApp)
|
||||
continue
|
||||
}
|
||||
itemAppImage.Name = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(stdout), "\n", ""), "image: ", "")
|
||||
if !hasDockerClient {
|
||||
itemApp.Children = append(itemApp.Children, itemAppImage)
|
||||
data = append(data, itemApp)
|
||||
continue
|
||||
}
|
||||
for _, imageItem := range imageList {
|
||||
for _, tag := range imageItem.RepoTags {
|
||||
if tag == itemAppImage.Name {
|
||||
itemAppImage.Size = uint64(imageItem.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
itemApp.Children = append(itemApp.Children, itemAppImage)
|
||||
data = append(data, itemApp)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func loadPanelFile(fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
|
||||
var data []dto.DataTree
|
||||
snapFiles, err := os.ReadDir(path.Join(global.CONF.System.BaseDir, "1panel"))
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
for _, fileItem := range snapFiles {
|
||||
itemData := dto.DataTree{
|
||||
ID: uuid.NewString(),
|
||||
Label: fileItem.Name(),
|
||||
IsCheck: true,
|
||||
Path: path.Join(global.CONF.System.BaseDir, "1panel", fileItem.Name()),
|
||||
}
|
||||
switch itemData.Label {
|
||||
case "agent", "conf", "db", "runtime", "secret":
|
||||
itemData.IsDisable = true
|
||||
case "log", "docker", "task", "clamav":
|
||||
panelPath := path.Join(global.CONF.System.BaseDir, "1panel", itemData.Label)
|
||||
itemData.Children, _ = loadFile(panelPath, 5, fileOp)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if fileItem.IsDir() {
|
||||
sizeItem, err := fileOp.GetDirSize(path.Join(global.CONF.System.BaseDir, "1panel", itemData.Label))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
itemData.Size = uint64(sizeItem)
|
||||
} else {
|
||||
fileInfo, err := fileItem.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
itemData.Size = uint64(fileInfo.Size())
|
||||
}
|
||||
if itemData.IsCheck && itemData.Size == 0 {
|
||||
itemData.IsCheck = false
|
||||
itemData.IsDisable = true
|
||||
}
|
||||
|
||||
data = append(data, itemData)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func loadFile(pathItem string, index int, fileOp fileUtils.FileOp) ([]dto.DataTree, error) {
|
||||
var data []dto.DataTree
|
||||
snapFiles, err := os.ReadDir(pathItem)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
i := 0
|
||||
for _, fileItem := range snapFiles {
|
||||
itemData := dto.DataTree{
|
||||
ID: uuid.NewString(),
|
||||
Label: fileItem.Name(),
|
||||
Name: fileItem.Name(),
|
||||
Path: path.Join(pathItem, fileItem.Name()),
|
||||
IsCheck: true,
|
||||
}
|
||||
if fileItem.IsDir() {
|
||||
sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, itemData.Label))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
itemData.Size = uint64(sizeItem)
|
||||
itemData.Children, _ = loadFile(path.Join(pathItem, itemData.Label), index-1, fileOp)
|
||||
} else {
|
||||
fileInfo, err := fileItem.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
itemData.Size = uint64(fileInfo.Size())
|
||||
}
|
||||
data = append(data, itemData)
|
||||
i++
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
@ -6,145 +6,219 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type snapHelper struct {
|
||||
SnapID uint
|
||||
Status *model.SnapshotStatus
|
||||
Ctx context.Context
|
||||
FileOp files.FileOp
|
||||
Wg *sync.WaitGroup
|
||||
SnapID uint
|
||||
snapAgentDB *gorm.DB
|
||||
snapCoreDB *gorm.DB
|
||||
Status *model.SnapshotStatus
|
||||
Ctx context.Context
|
||||
FileOp files.FileOp
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func snapJson(snap snapHelper, snapJson SnapshotJson, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": constant.Running})
|
||||
status := constant.StatusDone
|
||||
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t")
|
||||
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil {
|
||||
status = err.Error()
|
||||
func loadDbConn(snap *snapHelper, targetDir string, req dto.SnapshotCreate) error {
|
||||
global.LOG.Debug("start load snapshot db conn")
|
||||
|
||||
if err := snap.FileOp.CopyDir(path.Join(global.CONF.System.BaseDir, "1panel/db"), targetDir); err != nil {
|
||||
return err
|
||||
}
|
||||
snap.Status.PanelInfo = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status})
|
||||
agentDb, err := newSnapDB(path.Join(targetDir, "db"), "agent.db")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap.snapAgentDB = agentDb
|
||||
coreDb, err := newSnapDB(path.Join(targetDir, "db"), "core.db")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap.snapCoreDB = coreDb
|
||||
|
||||
if !req.WithMonitorData {
|
||||
_ = os.Remove(path.Join(targetDir, "db/monitor.db"))
|
||||
}
|
||||
if !req.WithOperationLog {
|
||||
_ = snap.snapCoreDB.Exec("DELETE FROM operation_logs")
|
||||
}
|
||||
if !req.WithLoginLog {
|
||||
_ = snap.snapCoreDB.Exec("DELETE FROM login_logs")
|
||||
}
|
||||
if err := snap.snapAgentDB.Where("id = ?", snap.SnapID).Delete(&model.Snapshot{}).Error; err != nil {
|
||||
global.LOG.Errorf("delete current snapshot record failed, err: %v", err)
|
||||
}
|
||||
if err := snap.snapAgentDB.Where("snap_id = ?", snap.SnapID).Delete(&model.SnapshotStatus{}).Error; err != nil {
|
||||
global.LOG.Errorf("delete current snapshot status record failed, err: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapPanel(snap snapHelper, targetDir string) {
|
||||
func snapBaseData(snap snapHelper, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"base_data": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil {
|
||||
if err := common.CopyFile("/usr/local/bin/1panel", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
if err := common.CopyFile("/usr/local/bin/1panel_agent", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
|
||||
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
|
||||
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.Panel = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
|
||||
}
|
||||
|
||||
func snapDaemonJson(snap snapHelper, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
status := constant.StatusDone
|
||||
if !snap.FileOp.Stat("/etc/docker/daemon.json") {
|
||||
snap.Status.DaemonJson = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
|
||||
return
|
||||
}
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running})
|
||||
if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil {
|
||||
if err := common.CopyFile("/etc/systemd/system/1panel_agent.service", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.DaemonJson = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status})
|
||||
|
||||
if snap.FileOp.Stat("/etc/docker/daemon.json") {
|
||||
if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
remarkInfo, _ := json.MarshalIndent(SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: global.CONF.System.Backup,
|
||||
}, "", "\t")
|
||||
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.BaseData = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"base_data": status})
|
||||
}
|
||||
|
||||
func snapAppData(snap snapHelper, targetDir string) {
|
||||
func snapAppImage(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running})
|
||||
appInstalls, err := appInstallRepo.ListBy()
|
||||
if err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
}
|
||||
runtimes, err := runtimeRepo.List()
|
||||
if err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()})
|
||||
return
|
||||
}
|
||||
imageRegex := regexp.MustCompile(`image:\s*(.*)`)
|
||||
var imageSaveList []string
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": constant.Running})
|
||||
|
||||
var imageList []string
|
||||
existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG")
|
||||
existImages := strings.Split(existStr, "\n")
|
||||
duplicateMap := make(map[string]bool)
|
||||
for _, app := range appInstalls {
|
||||
matches := imageRegex.FindAllStringSubmatch(app.DockerCompose, -1)
|
||||
for _, match := range matches {
|
||||
for _, existImage := range existImages {
|
||||
if match[1] == existImage && !duplicateMap[match[1]] {
|
||||
imageSaveList = append(imageSaveList, match[1])
|
||||
duplicateMap[match[1]] = true
|
||||
for _, app := range req.AppData {
|
||||
for _, item := range app.Children {
|
||||
if item.Label == "appImage" && item.IsCheck {
|
||||
for _, existImage := range existImages {
|
||||
if existImage == item.Name {
|
||||
imageList = append(imageList, item.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, runtime := range runtimes {
|
||||
for _, existImage := range existImages {
|
||||
if runtime.Image == existImage && !duplicateMap[runtime.Image] {
|
||||
imageSaveList = append(imageSaveList, runtime.Image)
|
||||
duplicateMap[runtime.Image] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(imageSaveList) != 0 {
|
||||
global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
|
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar"))
|
||||
if len(imageList) != 0 {
|
||||
global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz"))
|
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageList, " "), path.Join(targetDir, "images.tar.gz"))
|
||||
if err != nil {
|
||||
snap.Status.AppData = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std})
|
||||
snap.Status.AppImage = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": std})
|
||||
return
|
||||
}
|
||||
}
|
||||
snap.Status.AppData = constant.StatusDone
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone})
|
||||
snap.Status.AppImage = constant.StatusDone
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_image": constant.StatusDone})
|
||||
}
|
||||
|
||||
func snapBackup(snap snapHelper, targetDir string) {
|
||||
func snapBackupData(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := handleSnapTar(global.CONF.System.Backup, targetDir, "1panel_backup.tar.gz", "./system;./system_snapshot;", ""); err != nil {
|
||||
|
||||
excludes := loadBackupExcludes(snap, req.BackupData)
|
||||
for _, item := range req.AppData {
|
||||
for _, itemApp := range item.Children {
|
||||
if itemApp.Label == "appBackup" {
|
||||
excludes = append(excludes, loadAppBackupExcludes([]dto.DataTree{itemApp})...)
|
||||
}
|
||||
}
|
||||
}
|
||||
global.LOG.Debugf("excludes backup file: %v\n", strings.Join(excludes, "\n"))
|
||||
|
||||
if err := snap.FileOp.TarGzCompressPro(false, global.CONF.System.Backup, path.Join(targetDir, "1panel_backup.tar.gz"), "", strings.Join(excludes, ";")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.BackupData = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status})
|
||||
}
|
||||
func loadBackupExcludes(snap snapHelper, req []dto.DataTree) []string {
|
||||
var excludes []string
|
||||
for _, item := range req {
|
||||
if len(item.Children) == 0 {
|
||||
if item.IsCheck {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(item.Path, path.Join(global.CONF.System.Backup, "system_snapshot")) {
|
||||
fmt.Println(strings.TrimSuffix(item.Name, ".tar.gz"))
|
||||
if err := snap.snapAgentDB.Debug().Where("name = ? AND download_account_id = ?", strings.TrimSuffix(item.Name, ".tar.gz"), "1").Delete(&model.Snapshot{}).Error; err != nil {
|
||||
global.LOG.Errorf("delete snapshot from database failed, err: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path))
|
||||
if err := snap.snapAgentDB.Debug().Where("file_dir = ? AND file_name = ?", strings.TrimPrefix(path.Dir(item.Path), global.CONF.System.Backup+"/"), path.Base(item.Path)).Delete(&model.BackupRecord{}).Error; err != nil {
|
||||
global.LOG.Errorf("delete backup file from database failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
fmt.Println(strings.TrimPrefix(item.Path, global.CONF.System.Backup))
|
||||
excludes = append(excludes, "."+strings.TrimPrefix(item.Path, global.CONF.System.Backup))
|
||||
} else {
|
||||
excludes = append(excludes, loadBackupExcludes(snap, item.Children)...)
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
}
|
||||
func loadAppBackupExcludes(req []dto.DataTree) []string {
|
||||
var excludes []string
|
||||
for _, item := range req {
|
||||
if len(item.Children) == 0 {
|
||||
if !item.IsCheck {
|
||||
excludes = append(excludes, "."+strings.TrimPrefix(item.Path, path.Join(global.CONF.System.Backup)))
|
||||
}
|
||||
} else {
|
||||
excludes = append(excludes, loadAppBackupExcludes(item.Children)...)
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
}
|
||||
|
||||
func snapPanelData(snap snapHelper, targetDir string) {
|
||||
func snapPanelData(snap snapHelper, req dto.SnapshotCreate, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": constant.Running})
|
||||
status := constant.StatusDone
|
||||
dataDir := path.Join(global.CONF.System.BaseDir, "1panel")
|
||||
exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;"
|
||||
if strings.Contains(global.CONF.System.Backup, dataDir) {
|
||||
exclusionRules += ("." + strings.ReplaceAll(global.CONF.System.Backup, dataDir, "") + ";")
|
||||
|
||||
excludes := loadPanelExcludes(req.PanelData)
|
||||
for _, item := range req.AppData {
|
||||
for _, itemApp := range item.Children {
|
||||
if itemApp.Label == "appData" {
|
||||
excludes = append(excludes, loadPanelExcludes([]dto.DataTree{itemApp})...)
|
||||
}
|
||||
}
|
||||
}
|
||||
global.LOG.Debugf("excludes panel file: %v\n", strings.Join(excludes, "\n"))
|
||||
excludes = append(excludes, "./tmp")
|
||||
excludes = append(excludes, "./cache")
|
||||
excludes = append(excludes, "./uploads")
|
||||
excludes = append(excludes, "./db")
|
||||
excludes = append(excludes, "./resource")
|
||||
rootDir := path.Join(global.CONF.System.BaseDir, "1panel")
|
||||
if strings.Contains(global.CONF.System.Backup, rootDir) {
|
||||
excludes = append(excludes, "."+strings.ReplaceAll(global.CONF.System.Backup, rootDir, ""))
|
||||
}
|
||||
ignoreVal, _ := settingRepo.Get(settingRepo.WithByKey("SnapshotIgnore"))
|
||||
rules := strings.Split(ignoreVal.Value, ",")
|
||||
|
@ -152,27 +226,37 @@ func snapPanelData(snap snapHelper, targetDir string) {
|
|||
if len(ignore) == 0 || cmd.CheckIllegal(ignore) {
|
||||
continue
|
||||
}
|
||||
exclusionRules += ("." + strings.ReplaceAll(ignore, dataDir, "") + ";")
|
||||
excludes = append(excludes, "."+strings.ReplaceAll(ignore, rootDir, ""))
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"})
|
||||
sysIP, _ := settingRepo.Get(settingRepo.WithByKey("SystemIP"))
|
||||
_ = settingRepo.Update("SystemIP", "")
|
||||
checkPointOfWal()
|
||||
if err := handleSnapTar(dataDir, targetDir, "1panel_data.tar.gz", exclusionRules, ""); err != nil {
|
||||
|
||||
_ = snap.snapAgentDB.Model(&model.Setting{}).Where("key = ?", "SystemIP").Updates(map[string]interface{}{"SystemIP": ""})
|
||||
|
||||
if err := snap.FileOp.TarGzCompressPro(false, rootDir, path.Join(targetDir, "1panel_data.tar.gz"), "", strings.Join(excludes, ";")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting})
|
||||
|
||||
snap.Status.PanelData = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status})
|
||||
_ = settingRepo.Update("SystemIP", sysIP.Value)
|
||||
}
|
||||
func loadPanelExcludes(req []dto.DataTree) []string {
|
||||
var excludes []string
|
||||
for _, item := range req {
|
||||
if len(item.Children) == 0 {
|
||||
if !item.IsCheck {
|
||||
excludes = append(excludes, "."+strings.TrimPrefix(item.Path, path.Join(global.CONF.System.BaseDir, "1panel")))
|
||||
}
|
||||
} else {
|
||||
excludes = append(excludes, loadPanelExcludes(item.Children)...)
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
}
|
||||
|
||||
func snapCompress(snap snapHelper, rootDir string, secret string) {
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusRunning})
|
||||
tmpDir := path.Join(global.CONF.System.TmpDir, "system")
|
||||
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir))
|
||||
if err := handleSnapTar(rootDir, tmpDir, fileName, "", secret); err != nil {
|
||||
if err := snap.FileOp.TarGzCompressPro(true, rootDir, path.Join(tmpDir, fileName), secret, ""); err != nil {
|
||||
snap.Status.Compress = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()})
|
||||
return
|
||||
|
@ -207,12 +291,12 @@ func snapUpload(snap snapHelper, accounts string, file string) {
|
|||
for _, item := range targetAccounts {
|
||||
global.LOG.Debugf("start upload snapshot to %s, path: %s", item, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file)))
|
||||
if _, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))); err != nil {
|
||||
global.LOG.Debugf("upload to %s failed, err: %v", item, err)
|
||||
global.LOG.Debugf("upload to %s failed, err: %v", accountMap[item].name, err)
|
||||
snap.Status.Upload = err.Error()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()})
|
||||
return
|
||||
}
|
||||
global.LOG.Debugf("upload to %s successful", item)
|
||||
global.LOG.Debugf("upload to %s successful", accountMap[item].name)
|
||||
}
|
||||
snap.Status.Upload = constant.StatusDone
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone})
|
||||
|
@ -221,60 +305,25 @@ func snapUpload(snap snapHelper, accounts string, file string) {
|
|||
_ = os.Remove(source)
|
||||
}
|
||||
|
||||
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret string) error {
|
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exMap := make(map[string]struct{})
|
||||
exStr := ""
|
||||
excludes := strings.Split(exclusionRules, ";")
|
||||
excludes = append(excludes, "*.sock")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := exMap[exclude]; ok {
|
||||
continue
|
||||
}
|
||||
exStr += " --exclude "
|
||||
exStr += exclude
|
||||
exMap[exclude] = struct{}{}
|
||||
}
|
||||
path := ""
|
||||
if strings.Contains(sourceDir, "/") {
|
||||
itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "")
|
||||
aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")]
|
||||
if len(aheadDir) == 0 {
|
||||
aheadDir = "/"
|
||||
}
|
||||
path += fmt.Sprintf("-C %s %s", aheadDir, itemDir)
|
||||
} else {
|
||||
path = sourceDir
|
||||
}
|
||||
commands := ""
|
||||
if len(secret) != 0 {
|
||||
extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out"
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+exStr, path, extraCmd, targetDir+"/"+name)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
|
||||
func newSnapDB(dir, file string) (*gorm.DB, error) {
|
||||
db, _ := gorm.Open(sqlite.Open(path.Join(dir, file)), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
if len(stdout) != 0 {
|
||||
global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err)
|
||||
return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
sqlDB.SetConnMaxIdleTime(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
// global.LOG.Debug("load snapshot db conn successful!")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func checkPointOfWal() {
|
||||
if err := global.DB.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||
global.LOG.Errorf("handle check point failed, err: %v", err)
|
||||
func closeDatabase(db *gorm.DB) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
|
|
|
@ -1,124 +1,98 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover bool, req dto.SnapshotRecover) {
|
||||
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, req dto.SnapshotRecover) {
|
||||
_ = global.Cron.Stop()
|
||||
defer func() {
|
||||
global.Cron.Start()
|
||||
}()
|
||||
|
||||
snapFileDir := ""
|
||||
if isRecover {
|
||||
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
|
||||
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(baseDir, os.ModePerm)
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload {
|
||||
if err := handleDownloadSnapshot(snap, baseDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debugf("download snapshot file to %s successful!", baseDir)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "Decompress" {
|
||||
if err := handleUnTar(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, req.Secret); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("decompress snapshot file successful!", baseDir)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "Backup" {
|
||||
if err := backupBeforeRecover(snap); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("handle backup before recover successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
snapFileDir = fmt.Sprintf("%s/%s", baseDir, snap.Name)
|
||||
if _, err := os.Stat(snapFileDir); err != nil {
|
||||
snapFileDir = baseDir
|
||||
}
|
||||
} else {
|
||||
snapFileDir = fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
|
||||
if _, err := os.Stat(snapFileDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "", constant.StatusFailed, fmt.Sprintf("cannot find the backup file %s, please try to recover again.", snapFileDir))
|
||||
fileOp := files.NewFileOp()
|
||||
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
|
||||
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(baseDir, os.ModePerm)
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload {
|
||||
if err := handleDownloadSnapshot(snap, baseDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, "Download", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debugf("download snapshot file to %s successful!", baseDir)
|
||||
req.IsNew = true
|
||||
}
|
||||
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", snapFileDir))
|
||||
if req.IsNew || snap.InterruptStep == "Decompress" {
|
||||
if err := fileOp.TarGzExtractPro(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, req.Secret); err != nil {
|
||||
updateRecoverStatus(snap.ID, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("decompress snapshot file successful!", baseDir)
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "Backup" {
|
||||
if err := backupBeforeRecover(snap.Name); err != nil {
|
||||
updateRecoverStatus(snap.ID, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("handle backup before recover successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
snapFileDir := fmt.Sprintf("%s/%s", baseDir, snap.Name)
|
||||
if _, err := os.Stat(snapFileDir); err != nil {
|
||||
snapFileDir = baseDir
|
||||
}
|
||||
snapJson, err := u.readFromJson(path.Join(snapFileDir, "base/snapshot.json"))
|
||||
if err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
|
||||
updateRecoverStatus(snap.ID, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
if snap.InterruptStep == "Readjson" {
|
||||
req.IsNew = true
|
||||
}
|
||||
if isRecover && (req.IsNew || snap.InterruptStep == "AppData") {
|
||||
if req.IsNew || snap.InterruptStep == "AppImage" {
|
||||
if err := recoverAppData(snapFileDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "DockerDir", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err))
|
||||
updateRecoverStatus(snap.ID, "AppImage", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover app data from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "DaemonJson" {
|
||||
fileOp := files.NewFileOp()
|
||||
if err := recoverDaemonJson(snapFileDir, fileOp); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "DaemonJson", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover daemon.json from snapshot file successful!")
|
||||
global.LOG.Debug("recover app images from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "1PanelBinary" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
|
||||
if req.IsNew || snap.InterruptStep == "BaseData" {
|
||||
if err := recoverBaseData(path.Join(snapFileDir, "base"), fileOp); err != nil {
|
||||
updateRecoverStatus(snap.ID, "BaseData", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel binary from snapshot file successful!")
|
||||
global.LOG.Debug("recover base data from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "1PctlBinary" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "DBData" {
|
||||
if err := recoverDBData(path.Join(snapFileDir, "db"), fileOp); err != nil {
|
||||
updateRecoverStatus(snap.ID, "DBData", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1pctl from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "1PanelService" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel service from snapshot file successful!")
|
||||
global.LOG.Debug("recover db data from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "1PanelBackups" {
|
||||
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_backup.tar.gz"), snapJson.BackupDataDir, ""); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelBackups", constant.StatusFailed, err.Error())
|
||||
if err := fileOp.TarGzExtractPro(path.Join(snapFileDir, "/1panel_backup.tar.gz"), snapJson.BackupDataDir, ""); err != nil {
|
||||
updateRecoverStatus(snap.ID, "1PanelBackups", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel backups from snapshot file successful!")
|
||||
|
@ -126,9 +100,8 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
|
|||
}
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "1PanelData" {
|
||||
checkPointOfWal()
|
||||
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), ""); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelData", constant.StatusFailed, err.Error())
|
||||
if err := fileOp.TarGzExtractPro(path.Join(snapFileDir, "/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), ""); err != nil {
|
||||
updateRecoverStatus(snap.ID, "1PanelData", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1panel data from snapshot file successful!")
|
||||
|
@ -138,51 +111,11 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
|
|||
restartCompose(path.Join(snapJson.BaseDir, "1panel/docker/compose"))
|
||||
|
||||
global.LOG.Info("recover successful")
|
||||
if !isRecover {
|
||||
oriPath := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
|
||||
global.LOG.Debugf("remove the file %s after the operation is successful", oriPath)
|
||||
_ = os.RemoveAll(oriPath)
|
||||
} else {
|
||||
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
|
||||
_ = os.RemoveAll(path.Dir(snapFileDir))
|
||||
}
|
||||
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
|
||||
_ = os.RemoveAll(path.Dir(snapFileDir))
|
||||
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
|
||||
}
|
||||
|
||||
func backupBeforeRecover(snap model.Snapshot) error {
|
||||
baseDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
|
||||
var wg sync.WaitGroup
|
||||
var status model.SnapshotStatus
|
||||
itemHelper := snapHelper{SnapID: 0, Status: &status, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
|
||||
|
||||
jsonItem := SnapshotJson{
|
||||
BaseDir: global.CONF.System.BaseDir,
|
||||
BackupDataDir: global.CONF.System.Backup,
|
||||
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
|
||||
}
|
||||
_ = os.MkdirAll(path.Join(baseDir, "1panel"), os.ModePerm)
|
||||
_ = os.MkdirAll(path.Join(baseDir, "docker"), os.ModePerm)
|
||||
|
||||
wg.Add(4)
|
||||
itemHelper.Wg = &wg
|
||||
go snapJson(itemHelper, jsonItem, baseDir)
|
||||
go snapPanel(itemHelper, path.Join(baseDir, "1panel"))
|
||||
go snapDaemonJson(itemHelper, path.Join(baseDir, "docker"))
|
||||
go snapBackup(itemHelper, path.Join(baseDir, "1panel"))
|
||||
wg.Wait()
|
||||
itemHelper.Status.AppData = constant.StatusDone
|
||||
|
||||
allDone, msg := checkAllDone(status)
|
||||
if !allDone {
|
||||
return errors.New(msg)
|
||||
}
|
||||
snapPanelData(itemHelper, path.Join(baseDir, "1panel"))
|
||||
if status.PanelData != constant.StatusDone {
|
||||
return errors.New(status.PanelData)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error {
|
||||
account, client, err := NewBackupClientWithID(snap.DownloadAccountID)
|
||||
if err != nil {
|
||||
|
@ -202,18 +135,34 @@ func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error {
|
|||
}
|
||||
|
||||
func recoverAppData(src string) error {
|
||||
if _, err := os.Stat(path.Join(src, "docker/docker_image.tar")); err != nil {
|
||||
if _, err := os.Stat(path.Join(src, "images.tar.gz")); err != nil {
|
||||
global.LOG.Debug("no such docker images in snapshot")
|
||||
return nil
|
||||
}
|
||||
std, err := cmd.Execf("docker load < %s", path.Join(src, "docker/docker_image.tar"))
|
||||
std, err := cmd.Execf("docker load < %s", path.Join(src, "images.tar.gz"))
|
||||
if err != nil {
|
||||
return errors.New(std)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func recoverDaemonJson(src string, fileOp files.FileOp) error {
|
||||
func recoverBaseData(src string, fileOp files.FileOp) error {
|
||||
if err := fileOp.CopyFile(path.Join(src, "1pctl"), "/usr/local/bin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(src, "1panel"), "/usr/local/bin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(src, "1panel_agent"), "/usr/local/bin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(src, "1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(src, "1panel_agent.service"), "/etc/systemd/system"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
daemonJsonPath := "/etc/docker/daemon.json"
|
||||
_, errSrc := os.Stat(path.Join(src, "docker/daemon.json"))
|
||||
_, errPath := os.Stat(daemonJsonPath)
|
||||
|
@ -231,14 +180,8 @@ func recoverDaemonJson(src string, fileOp files.FileOp) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func recoverPanel(src string, dst string) error {
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return fmt.Errorf("file is not found in %s, err: %v", src, err)
|
||||
}
|
||||
if err := common.CopyFile(src, dst); err != nil {
|
||||
return fmt.Errorf("cp file failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
func recoverDBData(src string, fileOp files.FileOp) error {
|
||||
return fileOp.CopyDir(src, path.Join(global.CONF.System.BaseDir, "1panel", "db"))
|
||||
}
|
||||
|
||||
func restartCompose(composePath string) {
|
||||
|
@ -259,3 +202,86 @@ func restartCompose(composePath string) {
|
|||
}
|
||||
global.LOG.Debug("restart all compose successful!")
|
||||
}
|
||||
|
||||
func backupBeforeRecover(name string) error {
|
||||
rootDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, name)
|
||||
baseDir := path.Join(rootDir, "base")
|
||||
if _, err := os.Stat(baseDir); err != nil {
|
||||
_ = os.MkdirAll(baseDir, os.ModePerm)
|
||||
}
|
||||
|
||||
FileOp := files.NewFileOp()
|
||||
if err := FileOp.CopyDirWithExclude(path.Join(global.CONF.System.BaseDir, "1panel"), rootDir, []string{"cache", "tmp"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyDir(global.CONF.System.Backup, rootDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/usr/local/bin/1pctl", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/usr/local/bin/1panel", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/usr/local/bin/1panel_agent", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/etc/systemd/system/1panel.service", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/etc/systemd/system/1panel_agent.service", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile("/etc/docker/daemon.json", baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRollback(name string) error {
|
||||
rootDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, name)
|
||||
baseDir := path.Join(rootDir, "base")
|
||||
|
||||
FileOp := files.NewFileOp()
|
||||
if err := FileOp.CopyDir(path.Join(rootDir, "1panel"), global.CONF.System.BaseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyDir(path.Join(rootDir, "backup"), path.Dir(global.CONF.System.Backup)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "1pctl"), "/usr/local/bin/1pctl"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "1panel"), "/usr/local/bin/1panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "1panel_agent"), "/usr/local/bin/1panel_agent"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "1panel.service"), "/etc/systemd/system/1panel.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "1panel_agent.service"), "/etc/systemd/system/1panel_agent.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := FileOp.CopyFile(path.Join(baseDir, "daemon.json"), "/etc/docker/daemon.json"); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.RemoveAll(rootDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRecoverStatus(id uint, interruptStep, status, message string) {
|
||||
if status != constant.StatusSuccess {
|
||||
global.LOG.Errorf("recover failed, err: %s", message)
|
||||
}
|
||||
if err := snapshotRepo.Update(id, map[string]interface{}{
|
||||
"interrupt_step": interruptStep,
|
||||
"recover_status": status,
|
||||
"recover_message": message,
|
||||
"last_recovered_at": time.Now().Format(constant.DateTimeLayout),
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("update snap recover status failed, err: %v", err)
|
||||
}
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
}
|
||||
|
|
|
@ -72,18 +72,18 @@ func handleSnapStatus() {
|
|||
status, _ := snapRepo.GetStatusList()
|
||||
for _, item := range status {
|
||||
updates := make(map[string]interface{})
|
||||
if item.Panel == constant.StatusRunning {
|
||||
updates["panel"] = constant.StatusFailed
|
||||
}
|
||||
if item.PanelInfo == constant.StatusRunning {
|
||||
updates["panel_info"] = constant.StatusFailed
|
||||
}
|
||||
if item.DaemonJson == constant.StatusRunning {
|
||||
updates["daemon_json"] = constant.StatusFailed
|
||||
}
|
||||
if item.AppData == constant.StatusRunning {
|
||||
updates["app_data"] = constant.StatusFailed
|
||||
}
|
||||
// if item.Panel == constant.StatusRunning {
|
||||
// updates["panel"] = constant.StatusFailed
|
||||
// }
|
||||
// if item.PanelInfo == constant.StatusRunning {
|
||||
// updates["panel_info"] = constant.StatusFailed
|
||||
// }
|
||||
// if item.DaemonJson == constant.StatusRunning {
|
||||
// updates["daemon_json"] = constant.StatusFailed
|
||||
// }
|
||||
// if item.AppData == constant.StatusRunning {
|
||||
// updates["app_data"] = constant.StatusFailed
|
||||
// }
|
||||
if item.PanelData == constant.StatusRunning {
|
||||
updates["panel_data"] = constant.StatusFailed
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ func Init() {
|
|||
migrations.UpdateApp,
|
||||
migrations.AddTaskDB,
|
||||
migrations.UpdateAppInstall,
|
||||
migrations.UpdateSnapshot,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
|
|
@ -259,3 +259,10 @@ var UpdateAppInstall = &gormigrate.Migration{
|
|||
&model.AppInstall{})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateSnapshot = &gormigrate.Migration{
|
||||
ID: "20240913-update-snapshot",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.Snapshot{}, &model.SnapshotStatus{})
|
||||
},
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
|
||||
settingRouter.POST("/update", baseApi.UpdateSetting)
|
||||
|
||||
settingRouter.GET("/snapshot/load", baseApi.LoadSnapshotData)
|
||||
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||
settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus)
|
||||
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
|
||||
|
|
|
@ -450,6 +450,48 @@ func (f FileOp) CopyDir(src, dst string) error {
|
|||
return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/"))
|
||||
}
|
||||
|
||||
func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error {
|
||||
srcInfo, err := f.Fs.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDir := filepath.Join(dst, srcInfo.Name())
|
||||
if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(excludeNames) == 0 {
|
||||
return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/"))
|
||||
}
|
||||
tmpFiles, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range tmpFiles {
|
||||
isExclude := false
|
||||
for _, name := range excludeNames {
|
||||
if item.Name() == name {
|
||||
isExclude = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isExclude {
|
||||
continue
|
||||
}
|
||||
if item.IsDir() {
|
||||
fmt.Println(path.Join(src, item.Name()), dstDir)
|
||||
if err := f.CopyDir(path.Join(src, item.Name()), dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := f.CopyFile(path.Join(src, item.Name()), dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FileOp) CopyFile(src, dst string) error {
|
||||
dst = filepath.Clean(dst) + string(filepath.Separator)
|
||||
return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dst+"/"))
|
||||
|
@ -664,3 +706,56 @@ func ZipFile(files []archiver.File, dst afero.File) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FileOp) TarGzCompressPro(withDir bool, src, dst, secret, exclusionRules string) error {
|
||||
workdir := src
|
||||
srcItem := "."
|
||||
if withDir {
|
||||
workdir = path.Dir(src)
|
||||
srcItem = path.Base(src)
|
||||
}
|
||||
commands := ""
|
||||
|
||||
exMap := make(map[string]struct{})
|
||||
exStr := ""
|
||||
excludes := strings.Split(exclusionRules, ";")
|
||||
excludes = append(excludes, "*.sock")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := exMap[exclude]; ok {
|
||||
continue
|
||||
}
|
||||
exStr += " --exclude "
|
||||
exStr += exclude
|
||||
exMap[exclude] = struct{}{}
|
||||
}
|
||||
|
||||
if len(secret) != 0 {
|
||||
commands = fmt.Sprintf("tar -zcf - %s | openssl enc -aes-256-cbc -salt -pbkdf2 -k '%s' -out %s", srcItem, secret, dst)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar zcf %s %s %s", dst, exStr, srcItem)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
return cmd.ExecCmdWithDir(commands, workdir)
|
||||
}
|
||||
|
||||
func (f FileOp) TarGzExtractPro(src, dst string, secret string) error {
|
||||
if _, err := os.Stat(path.Dir(dst)); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(path.Dir(dst), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commands := ""
|
||||
if len(secret) != 0 {
|
||||
commands = fmt.Sprintf("openssl enc -d -aes-256-cbc -salt -pbkdf2 -k '%s' -in %s | tar -zxf - > /root/log", secret, src)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar zxvf %s", src)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
return cmd.ExecCmdWithDir(commands, dst)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -119,11 +119,18 @@ export namespace Setting {
|
|||
|
||||
export interface SnapshotCreate {
|
||||
id: number;
|
||||
from: string;
|
||||
fromAccounts: Array<string>;
|
||||
defaultDownload: string;
|
||||
sourceAccountIDs: string;
|
||||
downloadAccountID: string;
|
||||
description: string;
|
||||
secret: string;
|
||||
|
||||
appData: Array<DataTree>;
|
||||
panelData: Array<DataTree>;
|
||||
backupData: Array<DataTree>;
|
||||
|
||||
withMonitorData: boolean;
|
||||
withLoginLog: boolean;
|
||||
withOperationLog: boolean;
|
||||
}
|
||||
export interface SnapshotImport {
|
||||
from: string;
|
||||
|
@ -155,11 +162,31 @@ export namespace Setting {
|
|||
lastRollbackedAt: string;
|
||||
secret: string;
|
||||
}
|
||||
export interface SnapshotData {
|
||||
appData: Array<DataTree>;
|
||||
panelData: Array<DataTree>;
|
||||
backupData: Array<DataTree>;
|
||||
|
||||
withMonitorData: boolean;
|
||||
withLoginLog: boolean;
|
||||
withOperationLog: boolean;
|
||||
}
|
||||
export interface DataTree {
|
||||
id: string;
|
||||
label: string;
|
||||
key: string;
|
||||
name: string;
|
||||
size: number;
|
||||
isCheck: boolean;
|
||||
isDisable: boolean;
|
||||
|
||||
path: string;
|
||||
|
||||
Children: Array<DataTree>;
|
||||
}
|
||||
export interface SnapshotStatus {
|
||||
panel: string;
|
||||
panelInfo: string;
|
||||
daemonJson: string;
|
||||
appData: string;
|
||||
baseData: string;
|
||||
appImage: string;
|
||||
panelData: string;
|
||||
backupData: string;
|
||||
|
||||
|
|
|
@ -88,6 +88,9 @@ export const bindMFA = (param: Setting.MFABind) => {
|
|||
};
|
||||
|
||||
// snapshot
|
||||
export const loadSnapshotSetting = () => {
|
||||
return http.get<Setting.SnapshotData>(`/settings/snapshot/load`);
|
||||
};
|
||||
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
|
||||
return http.post(`/settings/snapshot`, param);
|
||||
};
|
||||
|
|
|
@ -1585,6 +1585,36 @@ const message = {
|
|||
'Backup files not in the current backup list, please try downloading from the file directory and importing for backup.',
|
||||
|
||||
snapshot: 'Snapshot',
|
||||
stepBaseData: 'Base Data',
|
||||
stepAppData: 'System Application',
|
||||
stepPanelData: 'System Data',
|
||||
stepBackupData: 'Backup Data',
|
||||
stepOtherData: 'Other Data',
|
||||
loginLog: 'Backup System Login Records',
|
||||
OperationLog: 'Backup System Operation Log Login Records',
|
||||
monitorData: 'Backup System Monitoring Data',
|
||||
selectAllImage: 'Backup All Application Images',
|
||||
agentLabel: 'Node Configuration',
|
||||
appDataLabel: 'Application Data',
|
||||
appImage: 'Application Image',
|
||||
appBackup: 'Application Backup',
|
||||
backupLabel: 'Backup Directory',
|
||||
cacheLabel: 'Cache Directory',
|
||||
confLabel: 'Configuration File',
|
||||
dbLabel: 'Database Directory',
|
||||
dockerLabel: 'Container Related Logs',
|
||||
logLabel: 'Log Directory',
|
||||
taskLabel: 'Scheduled Task Report',
|
||||
resourceLabel: 'Application Resource Directory',
|
||||
runtimeLabel: 'Runtime Directory',
|
||||
appLabel: 'Application',
|
||||
databaseLabel: 'Database',
|
||||
websiteLabel: 'Website',
|
||||
directoryLabel: 'Directory',
|
||||
appStoreLabel: 'Application Store',
|
||||
shellLabel: 'Script',
|
||||
tmpLabel: 'Temporary Directory',
|
||||
sslLabel: 'Certificate Directory',
|
||||
deleteHelper:
|
||||
'All backup files for the snapshot, including those in the third-party backup account, will be deleted.',
|
||||
status: 'Snapshot status',
|
||||
|
|
|
@ -1403,6 +1403,36 @@ const message = {
|
|||
backupJump: '未在當前備份列表中的備份檔案,請嘗試從檔案目錄中下載後導入備份。',
|
||||
|
||||
snapshot: '快照',
|
||||
stepBaseData: '基礎數據',
|
||||
stepAppData: '系統應用',
|
||||
stepPanelData: '系統數據',
|
||||
stepBackupData: '備份數據',
|
||||
stepOtherData: '其他數據',
|
||||
loginLog: '備份系統登錄記錄',
|
||||
OperationLog: '備份系統操作日誌登錄記錄',
|
||||
monitorData: '備份系統監控數據',
|
||||
selectAllImage: '備份所有應用鏡像',
|
||||
agentLabel: '節點配置',
|
||||
appDataLabel: '應用數據',
|
||||
appImage: '應用鏡像',
|
||||
appBackup: '應用備份',
|
||||
backupLabel: '備份目錄',
|
||||
cacheLabel: '緩存目錄',
|
||||
confLabel: '配置文件',
|
||||
dbLabel: '數據庫目錄',
|
||||
dockerLabel: '容器相關日誌',
|
||||
logLabel: '日誌目錄',
|
||||
taskLabel: '計劃任務報告',
|
||||
resourceLabel: '應用資源目錄',
|
||||
runtimeLabel: '運行時目錄',
|
||||
appLabel: '應用',
|
||||
databaseLabel: '數據庫',
|
||||
websiteLabel: '網站',
|
||||
directoryLabel: '目錄',
|
||||
appStoreLabel: '應用商店',
|
||||
shellLabel: '腳本',
|
||||
tmpLabel: '臨時目錄',
|
||||
sslLabel: '證書目錄',
|
||||
deleteHelper: '將刪除該快照的所有備份文件,包括第三方備份賬號中的文件。',
|
||||
status: '快照狀態',
|
||||
ignoreRule: '排除規則',
|
||||
|
|
|
@ -1405,6 +1405,36 @@ const message = {
|
|||
backupJump: '未在当前备份列表中的备份文件,请尝试从文件目录中下载后导入备份。',
|
||||
|
||||
snapshot: '快照',
|
||||
stepBaseData: '基础数据',
|
||||
stepAppData: '系统应用',
|
||||
stepPanelData: '系统数据',
|
||||
stepBackupData: '备份数据',
|
||||
stepOtherData: '其他数据',
|
||||
loginLog: '备份系统登陆记录',
|
||||
OperationLog: '备份系统备份系统操作日志登陆记录',
|
||||
monitorData: '备份系统监控数据',
|
||||
selectAllImage: '备份所有应用镜像',
|
||||
agentLabel: '节点配置',
|
||||
appDataLabel: '应用数据',
|
||||
appImage: '应用镜像',
|
||||
appBackup: '应用备份',
|
||||
backupLabel: '备份目录',
|
||||
cacheLabel: '缓存目录',
|
||||
confLabel: '配置文件',
|
||||
dbLabel: '数据库目录',
|
||||
dockerLabel: '容器相关日志',
|
||||
logLabel: '日志目录',
|
||||
taskLabel: '计划任务报告',
|
||||
resourceLabel: '应用资源目录',
|
||||
runtimeLabel: '运行时目录',
|
||||
appLabel: '应用',
|
||||
databaseLabel: '数据库',
|
||||
websiteLabel: '网站',
|
||||
directoryLabel: '目录',
|
||||
appStoreLabel: '应用商店',
|
||||
shellLabel: '脚本',
|
||||
tmpLabel: '临时目录',
|
||||
sslLabel: '证书目录',
|
||||
deleteHelper: '将删除该快照的所有备份文件,包括第三方备份账号中的文件。',
|
||||
ignoreRule: '排除规则',
|
||||
ignoreHelper: '快照时将使用该规则对 1Panel 数据目录进行压缩备份,请谨慎修改。',
|
||||
|
|
485
frontend/src/views/setting/snapshot/create/index.vue
Normal file
485
frontend/src/views/setting/snapshot/create/index.vue
Normal file
|
@ -0,0 +1,485 @@
|
|||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.snapshot')" :back="handleClose" size="large">
|
||||
<fu-steps
|
||||
v-loading="loading"
|
||||
direction="vertical"
|
||||
class="steps"
|
||||
:space="50"
|
||||
ref="stepsRef"
|
||||
:isLoading="stepLoading"
|
||||
:finishButtonText="$t('commons.button.create')"
|
||||
@finish="submitAddSnapshot"
|
||||
@change="changeStep"
|
||||
:beforeLeave="beforeLeave"
|
||||
>
|
||||
<fu-step id="baseData" :title="$t('setting.stepBaseData')">
|
||||
<el-form
|
||||
v-loading="loading"
|
||||
class="mt-5"
|
||||
label-position="top"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-form-item :label="$t('setting.backupAccount')" prop="fromAccounts">
|
||||
<el-select multiple @change="changeAccount" v-model="form.fromAccounts" clearable>
|
||||
<div v-for="item in backupOptions" :key="item.id">
|
||||
<el-option
|
||||
v-if="item.type !== $t('setting.LOCAL')"
|
||||
:value="item.id"
|
||||
:label="item.type + ' - ' + item.name"
|
||||
/>
|
||||
<el-option v-else :value="item.id" :label="item.type" />
|
||||
</div>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.default_download_path')" prop="downloadAccountID">
|
||||
<el-select v-model="form.downloadAccountID" clearable>
|
||||
<div v-for="item in accountOptions" :key="item.id">
|
||||
<el-option
|
||||
v-if="item.type !== $t('setting.LOCAL')"
|
||||
:value="item.id"
|
||||
:label="item.type + ' - ' + item.name"
|
||||
/>
|
||||
<el-option v-else :value="item.id" :label="item.type" />
|
||||
</div>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.compressPassword')" prop="secret">
|
||||
<el-input v-model="form.secret"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input type="textarea" clearable v-model="form.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</fu-step>
|
||||
<fu-step id="appData" :title="$t('setting.stepAppData')">
|
||||
<div>
|
||||
<el-checkbox
|
||||
class="ml-6"
|
||||
v-model="form.backupAllImage"
|
||||
@change="selectAllImage"
|
||||
:label="$t('setting.selectAllImage')"
|
||||
size="large"
|
||||
/>
|
||||
<el-tree
|
||||
style="max-width: 600px"
|
||||
ref="appRef"
|
||||
node-key="id"
|
||||
:default-expand-all="true"
|
||||
:data="form.appData"
|
||||
:props="defaultProps"
|
||||
show-checkbox
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="float-left">
|
||||
<span>{{ loadApp18n(data.label) }}</span>
|
||||
</div>
|
||||
<div class="ml-4 float-left">
|
||||
<span v-if="data.size">{{ computeSize(data.size) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</fu-step>
|
||||
<fu-step id="panelData" :title="$t('setting.stepPanelData')">
|
||||
<div>
|
||||
<el-tree
|
||||
style="max-width: 600px"
|
||||
ref="panelRef"
|
||||
node-key="id"
|
||||
:data="form.panelData"
|
||||
:props="defaultProps"
|
||||
show-checkbox
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="float-left">
|
||||
<span>{{ load18n(node, data.label) }}</span>
|
||||
</div>
|
||||
<div class="ml-4 float-left">
|
||||
<span v-if="data.size">{{ computeSize(data.size) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</fu-step>
|
||||
<fu-step id="backupData" :title="$t('setting.stepBackupData')">
|
||||
<div>
|
||||
<el-tree
|
||||
style="max-width: 600px"
|
||||
ref="backupRef"
|
||||
node-key="id"
|
||||
:data="form.backupData"
|
||||
:props="defaultProps"
|
||||
show-checkbox
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="float-left">
|
||||
<span>{{ load18n(node, data.label) }}</span>
|
||||
</div>
|
||||
<div class="ml-4 float-left">
|
||||
<span v-if="data.size">{{ computeSize(data.size) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</fu-step>
|
||||
<fu-step id="otherData" :title="$t('setting.stepOtherData')">
|
||||
<div class="ml-6">
|
||||
<el-checkbox v-model="form.withLoginLog" :label="$t('setting.loginLog')" size="large" />
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<el-checkbox v-model="form.withOperationLog" :label="$t('setting.OperationLog')" size="large" />
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<el-checkbox v-model="form.withMonitorData" :label="$t('setting.monitorData')" size="large" />
|
||||
</div>
|
||||
</fu-step>
|
||||
</fu-steps>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { loadSnapshotSetting, snapshotCreate } from '@/api/modules/setting';
|
||||
import { computeSize } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import { getBackupList } from '@/api/modules/backup';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref();
|
||||
const stepLoading = ref(false);
|
||||
const stepsRef = ref();
|
||||
|
||||
const appRef = ref();
|
||||
const panelRef = ref();
|
||||
const backupRef = ref();
|
||||
|
||||
const backupOptions = ref();
|
||||
const accountOptions = ref();
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
downloadAccountID: '',
|
||||
fromAccounts: [],
|
||||
sourceAccountIDs: '',
|
||||
description: '',
|
||||
secret: '',
|
||||
|
||||
backupAllImage: false,
|
||||
withLoginLog: false,
|
||||
withOperationLog: false,
|
||||
withMonitorData: false,
|
||||
|
||||
panelData: [],
|
||||
backupData: [],
|
||||
appData: [],
|
||||
});
|
||||
const rules = reactive({
|
||||
fromAccounts: [Rules.requiredSelect],
|
||||
downloadAccountID: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
checked: 'isCheck',
|
||||
disabled: 'isDisable',
|
||||
};
|
||||
const drawerVisible = ref();
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
const acceptParams = (): void => {
|
||||
search();
|
||||
loadBackups();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const submitForm = async (formEl: any) => {
|
||||
let bool;
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
bool = true;
|
||||
} else {
|
||||
bool = false;
|
||||
}
|
||||
});
|
||||
return bool;
|
||||
};
|
||||
const beforeLeave = async (stepItem: any) => {
|
||||
switch (stepItem.id) {
|
||||
case 'baseData':
|
||||
if (await submitForm(formRef.value)) {
|
||||
stepsRef.value.next();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case 'appData':
|
||||
let appChecks = appRef.value.getCheckedNodes();
|
||||
loadCheckForSubmit(appChecks, form.appData);
|
||||
return true;
|
||||
case 'panelData':
|
||||
let panelChecks = panelRef.value.getCheckedNodes();
|
||||
loadCheckForSubmit(panelChecks, form.panelData);
|
||||
return true;
|
||||
case 'backupData':
|
||||
let backupChecks = backupRef.value.getCheckedNodes();
|
||||
loadCheckForSubmit(backupChecks, form.backupData);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const loadApp18n = (label: string) => {
|
||||
switch (label) {
|
||||
case 'appData':
|
||||
return i18n.global.t('setting.appDataLabel');
|
||||
case 'appImage':
|
||||
case 'appBackup':
|
||||
return i18n.global.t('setting.' + label);
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
};
|
||||
|
||||
const loadBackups = async () => {
|
||||
const res = await getBackupList();
|
||||
backupOptions.value = [];
|
||||
for (const item of res.data) {
|
||||
if (item.id !== 0) {
|
||||
backupOptions.value.push({ id: item.id, type: i18n.global.t('setting.' + item.type), name: item.name });
|
||||
}
|
||||
}
|
||||
changeAccount();
|
||||
};
|
||||
|
||||
const changeStep = (currentStep: any) => {
|
||||
switch (currentStep.id) {
|
||||
case 'appData':
|
||||
if (appRef.value) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
setAppDefaultCheck(form.appData);
|
||||
});
|
||||
return;
|
||||
case 'panelData':
|
||||
if (panelRef.value) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
setPanelDefaultCheck(form.panelData);
|
||||
return;
|
||||
});
|
||||
return;
|
||||
case 'backupData':
|
||||
if (backupRef.value) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
setBackupDefaultCheck(form.backupData);
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const changeAccount = async () => {
|
||||
accountOptions.value = [];
|
||||
let isInAccounts = false;
|
||||
for (const item of backupOptions.value) {
|
||||
let exist = false;
|
||||
for (const ac of form.fromAccounts) {
|
||||
if (item.id == ac) {
|
||||
exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exist) {
|
||||
if (item.id === form.downloadAccountID) {
|
||||
isInAccounts = true;
|
||||
}
|
||||
accountOptions.value.push(item);
|
||||
}
|
||||
}
|
||||
if (!isInAccounts) {
|
||||
form.downloadAccountID = form.downloadAccountID ? undefined : form.downloadAccountID;
|
||||
}
|
||||
};
|
||||
|
||||
const load18n = (node: any, label: string) => {
|
||||
if (node.level === 1) {
|
||||
switch (label) {
|
||||
case 'agent':
|
||||
case 'conf':
|
||||
case 'db':
|
||||
case 'docker':
|
||||
case 'log':
|
||||
case 'runtime':
|
||||
case 'task':
|
||||
case 'app':
|
||||
case 'database':
|
||||
case 'website':
|
||||
case 'directory':
|
||||
return i18n.global.t('setting.' + label + 'Label');
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
}
|
||||
if (node.level === 2) {
|
||||
switch (label) {
|
||||
case 'App':
|
||||
return i18n.global.t('setting.appLabel');
|
||||
case 'AppStore':
|
||||
return i18n.global.t('setting.appStoreLabel');
|
||||
case 'shell':
|
||||
return i18n.global.t('setting.shellLabel');
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
const submitAddSnapshot = async () => {
|
||||
loading.value = true;
|
||||
form.sourceAccountIDs = form.fromAccounts.join(',');
|
||||
await snapshotCreate(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const loadCheckForSubmit = (checks: any, list: any) => {
|
||||
for (const item of list) {
|
||||
let isCheck = false;
|
||||
for (const check of checks) {
|
||||
if (item.id == check.id) {
|
||||
isCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.isCheck = isCheck;
|
||||
if (item.children) {
|
||||
loadCheckForSubmit(checks, item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectAllImage = () => {
|
||||
for (const item of form.appData) {
|
||||
for (const item2 of item.children) {
|
||||
if (item2.label === 'appImage') {
|
||||
appRef.value.setChecked(item2.id, form.backupAllImage, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const res = await loadSnapshotSetting();
|
||||
form.panelData = res.data.panelData || [];
|
||||
form.backupData = res.data.backupData || [];
|
||||
form.appData = res.data.appData || [];
|
||||
};
|
||||
|
||||
const setAppDefaultCheck = async (list: any) => {
|
||||
for (const item of list) {
|
||||
if (item.isCheck) {
|
||||
appRef.value.setChecked(item.id, true, true);
|
||||
continue;
|
||||
}
|
||||
if (item.children) {
|
||||
setAppDefaultCheck(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
const setPanelDefaultCheck = async (list: any) => {
|
||||
for (const item of list) {
|
||||
if (item.isCheck) {
|
||||
panelRef.value.setChecked(item.id, true, true);
|
||||
continue;
|
||||
}
|
||||
if (item.children) {
|
||||
setPanelDefaultCheck(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
const setBackupDefaultCheck = async (list: any) => {
|
||||
for (const item of list) {
|
||||
if (item.isCheck) {
|
||||
backupRef.value.setChecked(item.id, true, true);
|
||||
continue;
|
||||
}
|
||||
if (item.children) {
|
||||
setBackupDefaultCheck(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.steps {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
:deep(.el-step) {
|
||||
.el-step__line {
|
||||
background-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
.el-step__head.is-success {
|
||||
color: var(--el-color-primary-light-5);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
.el-step__icon {
|
||||
color: var(--el-color-primary-light-2);
|
||||
}
|
||||
.el-step__icon.is-text {
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
border-color: var(--el-color-primary-light-2);
|
||||
}
|
||||
|
||||
.el-step__title.is-finish {
|
||||
color: #717379;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-step__description.is-finish {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-step__title.is-success {
|
||||
font-weight: bold;
|
||||
color: var(--el-color-primary-light-2);
|
||||
}
|
||||
|
||||
.el-step__title.is-process {
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -88,7 +88,7 @@
|
|||
<el-button v-if="row.status === 'Failed'" type="danger" @click="onLoadStatus(row)" link>
|
||||
{{ $t('commons.status.error') }}
|
||||
</el-button>
|
||||
<el-tag v-if="row.status === 'Success'" type="success">
|
||||
<el-tag v-if="row.status === 'Success'" @click="onLoadStatus(row)" type="success">
|
||||
{{ $t('commons.status.success') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
@ -115,52 +115,8 @@
|
|||
</template>
|
||||
</LayoutContent>
|
||||
<RecoverStatus ref="recoverStatusRef" @search="search()"></RecoverStatus>
|
||||
<SnapshotCreate ref="createRef" @search="search()" />
|
||||
<SnapshotImport ref="importRef" @search="search()" />
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.createSnapshot')" :back="handleClose" size="large">
|
||||
<el-form
|
||||
v-loading="loading"
|
||||
label-position="top"
|
||||
ref="snapRef"
|
||||
label-width="100px"
|
||||
:model="snapInfo"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-form-item :label="$t('setting.backupAccount')" prop="fromAccounts">
|
||||
<el-select multiple @change="changeAccount" v-model="snapInfo.fromAccounts" clearable>
|
||||
<el-option
|
||||
v-for="item in backupOptions"
|
||||
:key="item.label"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.default_download_path')" prop="defaultDownload">
|
||||
<el-select v-model="snapInfo.defaultDownload" clearable>
|
||||
<el-option
|
||||
v-for="item in accountOptions"
|
||||
:key="item.label"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.compressPassword')" prop="secret">
|
||||
<el-input v-model="snapInfo.secret"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input type="textarea" clearable v-model="snapInfo.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="loading" @click="drawerVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="submitAddSnapshot(snapRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
|
@ -180,18 +136,17 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { snapshotCreate, searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
|
||||
import { searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { computeSize, dateFormat } from '@/utils/util';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import IgnoreRule from '@/views/setting/snapshot/ignore-rule/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { Setting } from '@/api/interface/setting';
|
||||
import IgnoreRule from '@/views/setting/snapshot/ignore-rule/index.vue';
|
||||
import SnapStatus from '@/views/setting/snapshot/snap_status/index.vue';
|
||||
import RecoverStatus from '@/views/setting/snapshot/status/index.vue';
|
||||
import SnapshotImport from '@/views/setting/snapshot/import/index.vue';
|
||||
import { getBackupList } from '@/api/modules/backup';
|
||||
import SnapshotCreate from '@/views/setting/snapshot/create/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref(false);
|
||||
|
@ -208,39 +163,15 @@ const searchName = ref();
|
|||
const opRef = ref();
|
||||
const ignoreRef = ref();
|
||||
|
||||
const createRef = ref();
|
||||
const snapStatusRef = ref();
|
||||
const recoverStatusRef = ref();
|
||||
const importRef = ref();
|
||||
const isRecordShow = ref();
|
||||
const backupOptions = ref();
|
||||
const accountOptions = ref();
|
||||
|
||||
const operateIDs = ref();
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const snapRef = ref<FormInstance>();
|
||||
const rules = reactive({
|
||||
fromAccounts: [Rules.requiredSelect],
|
||||
defaultDownload: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
let snapInfo = reactive<Setting.SnapshotCreate>({
|
||||
id: 0,
|
||||
from: '',
|
||||
defaultDownload: '',
|
||||
fromAccounts: [],
|
||||
description: '',
|
||||
secret: '',
|
||||
});
|
||||
const cleanData = ref();
|
||||
|
||||
const drawerVisible = ref<boolean>(false);
|
||||
|
||||
const onCreate = async () => {
|
||||
restForm();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const onImport = () => {
|
||||
let names = [];
|
||||
for (const item of data.value) {
|
||||
|
@ -249,12 +180,12 @@ const onImport = () => {
|
|||
importRef.value.acceptParams({ names: names });
|
||||
};
|
||||
|
||||
const onIgnore = () => {
|
||||
ignoreRef.value.acceptParams();
|
||||
const onCreate = () => {
|
||||
createRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
const onIgnore = () => {
|
||||
ignoreRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const onChange = async (info: any) => {
|
||||
|
@ -262,25 +193,6 @@ const onChange = async (info: any) => {
|
|||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
const submitAddSnapshot = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
snapInfo.from = snapInfo.fromAccounts.join(',');
|
||||
await snapshotCreate(snapInfo)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
search();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadStatus = (row: Setting.SnapshotInfo) => {
|
||||
snapStatusRef.value.acceptParams({
|
||||
id: row.id,
|
||||
|
@ -290,40 +202,6 @@ const onLoadStatus = (row: Setting.SnapshotInfo) => {
|
|||
});
|
||||
};
|
||||
|
||||
const loadBackups = async () => {
|
||||
const res = await getBackupList();
|
||||
backupOptions.value = [];
|
||||
for (const item of res.data) {
|
||||
if (item.id !== 0) {
|
||||
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
|
||||
}
|
||||
}
|
||||
changeAccount();
|
||||
};
|
||||
|
||||
const changeAccount = async () => {
|
||||
accountOptions.value = [];
|
||||
let isInAccounts = false;
|
||||
for (const item of backupOptions.value) {
|
||||
let exist = false;
|
||||
for (const ac of snapInfo.fromAccounts) {
|
||||
if (item.value == ac) {
|
||||
exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exist) {
|
||||
if (item.value === snapInfo.defaultDownload) {
|
||||
isInAccounts = true;
|
||||
}
|
||||
accountOptions.value.push(item);
|
||||
}
|
||||
}
|
||||
if (!isInAccounts) {
|
||||
snapInfo.defaultDownload = '';
|
||||
}
|
||||
};
|
||||
|
||||
const batchDelete = async (row: Setting.SnapshotInfo | null) => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
|
@ -362,11 +240,6 @@ const onSubmitDelete = async () => {
|
|||
});
|
||||
};
|
||||
|
||||
function restForm() {
|
||||
if (snapRef.value) {
|
||||
snapRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
|
@ -406,6 +279,5 @@ const search = async () => {
|
|||
|
||||
onMounted(() => {
|
||||
search();
|
||||
loadBackups();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,220 +1,295 @@
|
|||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.recoverDetail')" :back="handleClose" size="small">
|
||||
<el-form label-position="top" v-loading="loading">
|
||||
<span class="card-title">{{ $t('setting.recover') }}</span>
|
||||
<el-divider class="divider" />
|
||||
<div v-if="!snapInfo.recoverStatus && !snapInfo.lastRecoveredAt">
|
||||
<el-alert center class="alert" style="height: 257px" :closable="false">
|
||||
<el-button size="large" round plain type="primary" @click="recoverSnapshot(true)">
|
||||
{{ $t('setting.recover') }}
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@close="onClose"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('setting.status') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<el-alert :type="loadStatus(status.baseData)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.baseData)" link>{{ $t('setting.baseData') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.baseData)" class="top-margin">
|
||||
<span class="err-message">{{ status.baseData }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.appImage)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.appImage)" link>{{ $t('setting.appData') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.appImage)" class="top-margin">
|
||||
<span class="err-message">{{ status.appImage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.backupData)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.backupData)" link>{{ $t('setting.backupData') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.backupData)" class="top-margin">
|
||||
<span class="err-message">{{ status.backupData }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.panelData)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.panelData)" link>{{ $t('setting.panelData') }}</el-button>
|
||||
<div v-if="showErrorMsg(status.panelData)" class="top-margin">
|
||||
<span class="err-message">{{ status.panelData }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.compress)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.compress)" link>
|
||||
{{ $t('setting.compress') }} {{ status.size }}
|
||||
</el-button>
|
||||
</el-alert>
|
||||
</div>
|
||||
<el-card v-else class="mini-border-card">
|
||||
<div v-if="!snapInfo.recoverStatus" class="mini-border-card">
|
||||
<div v-if="snapInfo.lastRecoveredAt">
|
||||
<el-form-item :label="$t('commons.table.status')">
|
||||
<el-tag type="success">
|
||||
{{ $t('commons.table.statusSuccess') }}
|
||||
</el-tag>
|
||||
<el-button @click="recoverSnapshot(true)" style="margin-left: 10px" type="primary">
|
||||
{{ $t('setting.recover') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.lastRecoverAt')">
|
||||
{{ snapInfo.lastRecoveredAt }}
|
||||
</el-form-item>
|
||||
<div v-if="showErrorMsg(status.compress)" class="top-margin">
|
||||
<span class="err-message">{{ status.compress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item :label="$t('commons.table.status')">
|
||||
<el-tag type="danger" v-if="snapInfo.recoverStatus === 'Failed'">
|
||||
{{ $t('commons.table.statusFailed') }}
|
||||
</el-tag>
|
||||
<el-tag type="success" v-if="snapInfo.recoverStatus === 'Success'">
|
||||
{{ $t('commons.table.statusSuccess') }}
|
||||
</el-tag>
|
||||
<el-tag type="info" v-if="snapInfo.recoverStatus === 'Waiting'">
|
||||
{{ $t('commons.table.statusWaiting') }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.lastRecoverAt')" v-if="snapInfo.recoverStatus !== 'Waiting'">
|
||||
{{ snapInfo.lastRecoveredAt }}
|
||||
</el-form-item>
|
||||
<div v-if="snapInfo.recoverStatus === 'Failed'">
|
||||
<el-form-item :label="$t('commons.button.log')">
|
||||
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
|
||||
{{ snapInfo.recoverMessage }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="recoverSnapshot(false)" type="primary">
|
||||
{{ $t('commons.button.retry') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert :type="loadStatus(status.upload)" :closable="false">
|
||||
<template #title>
|
||||
<el-button :icon="loadIcon(status.upload)" link>
|
||||
{{ $t('setting.upload') }}
|
||||
</el-button>
|
||||
<div v-if="showErrorMsg(status.upload)" class="top-margin">
|
||||
<span class="err-message">{{ status.upload }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div v-if="snapInfo.recoverStatus === 'Failed'">
|
||||
<span class="card-title">{{ $t('setting.rollback') }}</span>
|
||||
<el-divider class="divider" />
|
||||
<div v-if="!snapInfo.rollbackStatus && !snapInfo.lastRollbackedAt">
|
||||
<el-alert center class="alert" style="height: 257px" :closable="false">
|
||||
<el-button size="large" round plain type="primary" @click="rollbackSnapshot()">
|
||||
{{ $t('setting.rollback') }}
|
||||
</el-button>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div v-if="!snapInfo.rollbackStatus">
|
||||
<div v-if="snapInfo.lastRollbackedAt">
|
||||
<el-form-item :label="$t('commons.table.status')">
|
||||
<el-tag type="success">
|
||||
{{ $t('commons.table.statusSuccess') }}
|
||||
</el-tag>
|
||||
<el-button @click="rollbackSnapshot" style="margin-left: 10px" type="primary">
|
||||
{{ $t('setting.rollback') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.lastRollbackAt')">
|
||||
{{ snapInfo.lastRollbackedAt }}
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item :label="$t('commons.table.status')">
|
||||
<el-tag type="success" v-if="snapInfo.rollbackStatus === 'Success'">
|
||||
{{ $t('commons.table.statusSuccess') }}
|
||||
</el-tag>
|
||||
<el-tag type="danger" v-if="snapInfo.rollbackStatus === 'Failed'">
|
||||
{{ $t('commons.table.statusFailed') }}
|
||||
</el-tag>
|
||||
<el-tag type="info" v-if="snapInfo.rollbackStatus === 'Waiting'">
|
||||
{{ $t('commons.table.statusWaiting') }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
style="margin-left: 15px"
|
||||
:disabled="snapInfo.rollbackStatus !== 'Success'"
|
||||
@click="rollbackSnapshot"
|
||||
>
|
||||
{{ $t('setting.rollback') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.lastRollbackAt')" v-if="snapInfo.rollbackStatus !== 'Waiting'">
|
||||
{{ snapInfo.lastRollbackedAt }}
|
||||
</el-form-item>
|
||||
<div v-if="snapInfo.rollbackStatus === 'Failed'">
|
||||
<el-form-item :label="$t('commons.button.log')">
|
||||
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
|
||||
{{ snapInfo.rollbackMessage }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="rollbackSnapshot()" type="primary">
|
||||
{{ $t('commons.button.retry') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</DrawerPro>
|
||||
<SnapRecover ref="recoverRef" @close="handleClose" />
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onClose">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-if="showRetry()" @click="onRetry">
|
||||
{{ $t('commons.button.retry') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { Setting } from '@/api/interface/setting';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { snapshotRollback } from '@/api/modules/setting';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { loadOsInfo } from '@/api/modules/dashboard';
|
||||
import SnapRecover from '@/views/setting/snapshot/recover/index.vue';
|
||||
import { loadSnapStatus, snapshotCreate } from '@/api/modules/setting';
|
||||
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
|
||||
const status = reactive<Setting.SnapshotStatus>({
|
||||
baseData: '',
|
||||
appImage: '',
|
||||
panelData: '',
|
||||
backupData: '',
|
||||
|
||||
compress: '',
|
||||
size: '',
|
||||
upload: '',
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const snapInfo = ref();
|
||||
const loading = ref();
|
||||
const snapID = ref();
|
||||
const snapFrom = ref();
|
||||
const snapDefaultDownload = ref();
|
||||
const snapDescription = ref();
|
||||
|
||||
const recoverRef = ref();
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
interface DialogProps {
|
||||
snapInfo: Setting.SnapshotInfo;
|
||||
id: number;
|
||||
from: string;
|
||||
defaultDownload: string;
|
||||
description: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
snapInfo.value = params.snapInfo;
|
||||
drawerVisible.value = true;
|
||||
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
dialogVisible.value = true;
|
||||
snapID.value = props.id;
|
||||
snapFrom.value = props.from;
|
||||
snapDefaultDownload.value = props.defaultDownload;
|
||||
snapDescription.value = props.description;
|
||||
onWatch();
|
||||
nextTick(() => {
|
||||
loadCurrentStatus();
|
||||
});
|
||||
};
|
||||
const emit = defineEmits(['search']);
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const recoverSnapshot = async (isNew: boolean) => {
|
||||
const loadCurrentStatus = async () => {
|
||||
loading.value = true;
|
||||
await loadOsInfo()
|
||||
await loadSnapStatus(snapID.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
let params = {
|
||||
id: snapInfo.value.id,
|
||||
isNew: isNew,
|
||||
name: snapInfo.value.name,
|
||||
reDownload: false,
|
||||
secret: snapInfo.value.secret,
|
||||
status.baseData = res.data.baseData;
|
||||
status.appImage = res.data.appImage;
|
||||
status.panelData = res.data.panelData;
|
||||
status.backupData = res.data.backupData;
|
||||
|
||||
arch: res.data.kernelArch,
|
||||
size: snapInfo.value.size,
|
||||
freeSize: res.data.diskSize,
|
||||
};
|
||||
recoverRef.value.acceptParams(params);
|
||||
status.compress = res.data.compress;
|
||||
status.size = res.data.size;
|
||||
status.upload = res.data.upload;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const rollbackSnapshot = async () => {
|
||||
ElMessageBox.confirm(i18n.global.t('setting.rollbackHelper'), i18n.global.t('setting.rollback'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
loading.value = true;
|
||||
await snapshotRollback({ id: snapInfo.value.id, isNew: false, reDownload: false, secret: '' })
|
||||
.then(() => {
|
||||
emit('search');
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
const onClose = async () => {
|
||||
emit('search');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const onRetry = async () => {
|
||||
loading.value = true;
|
||||
await snapshotCreate({
|
||||
id: snapID.value,
|
||||
description: snapDescription.value,
|
||||
|
||||
downloadAccountID: '',
|
||||
sourceAccountIDs: '',
|
||||
secret: '',
|
||||
|
||||
withLoginLog: false,
|
||||
withOperationLog: false,
|
||||
withMonitorData: false,
|
||||
|
||||
panelData: [],
|
||||
backupData: [],
|
||||
appData: [],
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
loadCurrentStatus();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onWatch = () => {
|
||||
timer = setInterval(async () => {
|
||||
if (keepLoadStatus()) {
|
||||
const res = await loadSnapStatus(snapID.value);
|
||||
status.baseData = res.data.baseData;
|
||||
status.appImage = res.data.appImage;
|
||||
status.panelData = res.data.panelData;
|
||||
status.backupData = res.data.backupData;
|
||||
|
||||
status.compress = res.data.compress;
|
||||
status.size = res.data.size;
|
||||
status.upload = res.data.upload;
|
||||
}
|
||||
}, 1000 * 3);
|
||||
};
|
||||
|
||||
const keepLoadStatus = () => {
|
||||
if (status.baseData === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.appImage === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelData === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.backupData === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.compress === 'Running') {
|
||||
return true;
|
||||
}
|
||||
if (status.upload === 'Uploading') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const showErrorMsg = (status: string) => {
|
||||
return status !== 'Running' && status !== 'Done' && status !== 'Uploading' && status !== 'Waiting';
|
||||
};
|
||||
|
||||
const showRetry = () => {
|
||||
if (keepLoadStatus()) {
|
||||
return false;
|
||||
}
|
||||
if (status.baseData !== 'Running' && status.baseData !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.appImage !== 'Running' && status.appImage !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.panelData !== 'Running' && status.panelData !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.backupData !== 'Running' && status.backupData !== 'Done') {
|
||||
return true;
|
||||
}
|
||||
if (status.compress !== 'Running' && status.compress !== 'Done' && status.compress !== 'Waiting') {
|
||||
return true;
|
||||
}
|
||||
if (status.upload !== 'Uploading' && status.upload !== 'Done' && status.upload !== 'Waiting') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadStatus = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Running':
|
||||
case 'Waiting':
|
||||
case 'Uploading':
|
||||
return 'info';
|
||||
case 'Done':
|
||||
return 'success';
|
||||
default:
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const loadIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Running':
|
||||
case 'Waiting':
|
||||
case 'Uploading':
|
||||
return 'Loading';
|
||||
case 'Done':
|
||||
return 'Check';
|
||||
default:
|
||||
return 'Close';
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divider {
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
margin: 12px 0;
|
||||
border-top: 1px var(--el-border-color) var(--el-border-style);
|
||||
<style scoped lang="scss">
|
||||
.el-alert {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.alert {
|
||||
background-color: rgba(0, 94, 235, 0.03);
|
||||
.el-alert:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 25px;
|
||||
color: var(--el-button-text-color, var(--el-text-color-regular));
|
||||
.top-margin {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.err-message {
|
||||
margin-left: 23px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue