diff --git a/backend/app.yaml b/backend/app.yaml index 3ac56716..20c856a7 100644 --- a/backend/app.yaml +++ b/backend/app.yaml @@ -1,41 +1,38 @@ +base_dir: /opt + system: port: 9999 db_type: sqlite - level: debug - data_dir: /opt/1Panel/data - app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps.json" - + data_dir: ${base_dir}/1Panel/data + cache: ${base_dir}/1Panel/data/cache + backup: ${base_dir}/1Panel/data/backup + app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps/list.json" sqlite: - path: /opt/1Panel/data/db + path: ${base_dir}/1Panel/data/db db_file: 1Panel.db log: - level: info + level: debug time_zone: Asia/Shanghai - path: /opt/1Panel/log + path: ${base_dir}/1Panel/log log_name: 1Panel log_suffix: .log - log_backup: 10 #最大日志保留个数 + log_backup: 10 -cache: - path: /opt/1Panel/data/cache - -# 跨域配置 cors: - mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + mode: whitelist whitelist: - allow-origin: example1.com allow-headers: content-type allow-methods: GET, POST expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 + allow-credentials: true - allow-origin: example2.com allow-headers: content-type allow-methods: GET, POST expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 + allow-credentials: true -# 加密设置 encrypt: - key: 1Panel123@2022!! \ No newline at end of file + key: 1Panel_key@2023! \ No newline at end of file diff --git a/backend/app/api/v1/snapshot.go b/backend/app/api/v1/snapshot.go index 8dca968b..043031ea 100644 --- a/backend/app/api/v1/snapshot.go +++ b/backend/app/api/v1/snapshot.go @@ -9,14 +9,14 @@ import ( ) // @Tags System Setting -// @Summary Create snapshot +// @Summary Create system backup // @Description 创建系统快照 // @Accept json // @Param request body dto.SnapshotCreate true "request" // @Success 200 // @Security ApiKeyAuth // @Router /settings/snapshot [post] -// @x-panel-log {"bodyKeys":["name", "description"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建系统快照 [name][description]","formatEN":"Create system snapshot [name][description]"} +// @x-panel-log {"bodyKeys":["from", "description"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建系统快照 [description] 到 [from]","formatEN":"Create system backup [description] to [from]"} func (b *BaseApi) CreateSnapshot(c *gin.Context) { var req dto.SnapshotCreate if err := c.ShouldBindJSON(&req); err != nil { @@ -27,7 +27,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - if err := snapshotService.Create(req); err != nil { + if err := snapshotService.SnapshotCreate(req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } @@ -41,7 +41,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) { // @Param request body dto.PageInfo true "request" // @Success 200 {object} dto.PageResult // @Security ApiKeyAuth -// @Router /websites/acme/search [post] +// @Router /settings/snapshot/search [post] func (b *BaseApi) SearchSnapshot(c *gin.Context) { var req dto.PageInfo if err := c.ShouldBindJSON(&req); err != nil { @@ -58,3 +58,84 @@ func (b *BaseApi) SearchSnapshot(c *gin.Context) { Items: accounts, }) } + +// @Tags System Setting +// @Summary Recover system backup +// @Description 从系统快照恢复 +// @Accept json +// @Param request body dto.SnapshotRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/recover [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 恢复","formatEN":"Recover from system backup [name]"} +func (b *BaseApi) RecoverSnapshot(c *gin.Context) { + var req dto.SnapshotRecover + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := snapshotService.SnapshotRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Rollback system backup +// @Description 从系统快照回滚 +// @Accept json +// @Param request body dto.SnapshotRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/rollback [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 回滚","formatEN":"Rollback from system backup [name]"} +func (b *BaseApi) RollbackSnapshot(c *gin.Context) { + var req dto.SnapshotRecover + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := snapshotService.SnapshotRollback(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Delete system backup +// @Description 删除系统快照 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"ids","isList":true,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"删除系统快照 [name]","formatEN":"Delete system backup [name]"} +func (b *BaseApi) DeleteSnapshot(c *gin.Context) { + var req dto.BatchDeleteReq + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := snapshotService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/backend/app/api/v1/upgrade.go b/backend/app/api/v1/upgrade.go new file mode 100644 index 00000000..0acb2927 --- /dev/null +++ b/backend/app/api/v1/upgrade.go @@ -0,0 +1,34 @@ +package v1 + +import ( + "context" + + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/gin-gonic/gin" + "github.com/google/go-github/github" +) + +// @Tags System Setting +// @Summary Load upgrade info +// @Description 加载系统更新信息 +// @Success 200 {object} dto.UpgradeInfo +// @Security ApiKeyAuth +// @Router /settings/upgrade [get] +func (b *BaseApi) GetUpgradeInfo(c *gin.Context) { + client := github.NewClient(nil) + stats, _, err := client.Repositories.GetLatestRelease(context.Background(), "KubeOperator", "KubeOperator") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + info := dto.UpgradeInfo{ + NewVersion: string(*stats.Name), + Tag: string(*stats.TagName), + ReleaseNote: string(*stats.Body), + CreatedAt: github.Timestamp(*stats.CreatedAt).Format("2006-01-02 15:04:05"), + PublishedAt: github.Timestamp(*stats.PublishedAt).Format("2006-01-02 15:04:05"), + } + helper.SuccessWithData(c, info) +} diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index ea2a2020..47ad3da9 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -3,8 +3,9 @@ package dto import "time" type SettingInfo struct { - UserName string `json:"userName"` - Email string `json:"email"` + UserName string `json:"userName"` + Email string `json:"email"` + SystemVersion string `json:"systemVersion"` SessionTimeout string `json:"sessionTimeout"` LocalTime string `json:"localTime"` @@ -41,15 +42,37 @@ type PasswordUpdate struct { } type SnapshotCreate struct { - BackupType string `json:"backupType" validate:"required,oneof=OSS S3 SFTP MINIO"` + From string `json:"from" validate:"required,oneof=OSS S3 SFTP MINIO"` Description string `json:"description"` } +type SnapshotRecover struct { + IsNew bool `json:"isNew"` + ReDownload bool `json:"reDownload"` + ID uint `json:"id" validate:"required"` +} type SnapshotInfo struct { ID uint `json:"id"` Name string `json:"name"` Description string `json:"description"` - BackupType string `json:"backupType"` + From string `json:"from"` Status string `json:"status"` Message string `json:"message"` CreatedAt time.Time `json:"createdAt"` + Version string `json:"version"` + + 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 UpgradeInfo struct { + NewVersion string `json:"newVersion"` + Tag string `json:"tag"` + ReleaseNote string `json:"releaseNote"` + CreatedAt string `json:"createdAt"` + PublishedAt string `json:"publishedAt"` } diff --git a/backend/app/model/cronjob.go b/backend/app/model/cronjob.go index a436fb31..0aae20e4 100644 --- a/backend/app/model/cronjob.go +++ b/backend/app/model/cronjob.go @@ -33,7 +33,7 @@ type Cronjob struct { type JobRecords struct { BaseModel - CronjobID uint `gorm:"type:varchar(64);not null" json:"cronjobID"` + CronjobID uint `gorm:"type:decimal" json:"cronjobID"` StartTime time.Time `gorm:"type:datetime" json:"startTime"` Interval float64 `gorm:"type:float" json:"interval"` Records string `gorm:"longtext" json:"records"` diff --git a/backend/app/model/snapshot.go b/backend/app/model/snapshot.go index 93476e9e..ce2e1c39 100644 --- a/backend/app/model/snapshot.go +++ b/backend/app/model/snapshot.go @@ -4,8 +4,16 @@ type Snapshot struct { BaseModel Name string `json:"name" gorm:"type:varchar(64);not null;unique"` Description string `json:"description" gorm:"type:varchar(256)"` - BackupType string `json:"backupType" gorm:"type:varchar(64)"` + From string `json:"from"` Status string `json:"status" gorm:"type:varchar(64)"` Message string `json:"message" gorm:"type:varchar(256)"` Version string `json:"version" gorm:"type:varchar(256)"` + + InterruptStep string `json:"interruptStep" gorm:"type:varchar(64)"` + RecoverStatus string `json:"recoverStatus" gorm:"type:varchar(64)"` + RecoverMessage string `json:"recoverMessage" gorm:"type:varchar(256)"` + LastRecoveredAt string `json:"lastRecoveredAt" gorm:"type:varchar(64)"` + RollbackStatus string `json:"rollbackStatus" gorm:"type:varchar(64)"` + RollbackMessage string `json:"rollbackMessage" gorm:"type:varchar(256)"` + LastRollbackedAt string `json:"lastRollbackedAt" gorm:"type:varchar(64)"` } diff --git a/backend/app/repo/common.go b/backend/app/repo/common.go index ecf88209..a521e38c 100644 --- a/backend/app/repo/common.go +++ b/backend/app/repo/common.go @@ -2,6 +2,7 @@ package repo import ( "context" + "time" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" @@ -17,6 +18,8 @@ type ICommonRepo interface { WithOrderBy(orderStr string) DBOption WithLikeName(name string) DBOption WithIdsIn(ids []uint) DBOption + WithByDate(startTime, endTime time.Time) DBOption + WithByStartDate(startTime time.Time) DBOption } type CommonRepo struct{} @@ -33,6 +36,18 @@ func (c *CommonRepo) WithByName(name string) DBOption { } } +func (c *CommonRepo) WithByDate(startTime, endTime time.Time) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("start_time > ? AND start_time < ?", startTime, endTime) + } +} + +func (c *CommonRepo) WithByStartDate(startTime time.Time) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("start_time < ?", startTime) + } +} + func (c *CommonRepo) WithByType(tp string) DBOption { return func(g *gorm.DB) *gorm.DB { return g.Where("type = ?", tp) diff --git a/backend/app/repo/cronjob.go b/backend/app/repo/cronjob.go index 300ca261..3c685201 100644 --- a/backend/app/repo/cronjob.go +++ b/backend/app/repo/cronjob.go @@ -19,7 +19,6 @@ type ICronjobRepo interface { List(opts ...DBOption) ([]model.Cronjob, error) Page(limit, offset int, opts ...DBOption) (int64, []model.Cronjob, error) Create(cronjob *model.Cronjob) error - WithByDate(startTime, endTime time.Time) DBOption WithByJobID(id int) DBOption Save(id uint, cronjob model.Cronjob) error Update(id uint, vars map[string]interface{}) error @@ -107,16 +106,6 @@ func (u *CronjobRepo) Create(cronjob *model.Cronjob) error { return global.DB.Create(cronjob).Error } -func (c *CronjobRepo) WithByDate(startTime, endTime time.Time) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("start_time > ? AND start_time < ?", startTime, endTime) - } -} -func (c *CronjobRepo) WithByStartDate(startTime time.Time) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("start_time < ?", startTime) - } -} func (c *CronjobRepo) WithByJobID(id int) DBOption { return func(g *gorm.DB) *gorm.DB { return g.Where("cronjob_id = ?", id) diff --git a/backend/app/repo/snapshot.go b/backend/app/repo/snapshot.go index 286e78ff..931fd4e2 100644 --- a/backend/app/repo/snapshot.go +++ b/backend/app/repo/snapshot.go @@ -7,9 +7,10 @@ import ( type ISnapshotRepo interface { Get(opts ...DBOption) (model.Snapshot, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error) - Create(snapshot *model.Snapshot) error + GetList(opts ...DBOption) ([]model.Snapshot, error) + Create(snap *model.Snapshot) error Update(id uint, vars map[string]interface{}) error + Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error) Delete(opts ...DBOption) error } @@ -20,13 +21,23 @@ func NewISnapshotRepo() ISnapshotRepo { type SnapshotRepo struct{} func (u *SnapshotRepo) Get(opts ...DBOption) (model.Snapshot, error) { - var snapshot model.Snapshot + var Snapshot model.Snapshot db := global.DB for _, opt := range opts { db = opt(db) } - err := db.First(&snapshot).Error - return snapshot, err + err := db.First(&Snapshot).Error + return Snapshot, err +} + +func (u *SnapshotRepo) GetList(opts ...DBOption) ([]model.Snapshot, error) { + var snaps []model.Snapshot + db := global.DB.Model(&model.Snapshot{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&snaps).Error + return snaps, err } func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Snapshot, error) { @@ -41,8 +52,8 @@ func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Sn return count, users, err } -func (u *SnapshotRepo) Create(snapshot *model.Snapshot) error { - return global.DB.Create(snapshot).Error +func (u *SnapshotRepo) Create(Snapshot *model.Snapshot) error { + return global.DB.Create(Snapshot).Error } func (u *SnapshotRepo) Update(id uint, vars map[string]interface{}) error { diff --git a/backend/app/service/cornjob.go b/backend/app/service/cornjob.go index 90a839f2..694fa6a5 100644 --- a/backend/app/service/cornjob.go +++ b/backend/app/service/cornjob.go @@ -67,7 +67,7 @@ func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interfac search.PageSize, commonRepo.WithByStatus(search.Status), cronjobRepo.WithByJobID(search.CronjobID), - cronjobRepo.WithByDate(search.StartTime, search.EndTime)) + commonRepo.WithByDate(search.StartTime, search.EndTime)) var dtoCronjobs []dto.Record for _, record := range records { var item dto.Record diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index f818264b..2a1954d7 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -235,6 +235,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error { } cmd := exec.Command("tar", exStr...) stdout, err := cmd.CombinedOutput() + fmt.Println(string(stdout)) if err != nil { return errors.New(string(stdout)) } diff --git a/backend/app/service/snapshot.go b/backend/app/service/snapshot.go index 1403014f..ecd6bfbb 100644 --- a/backend/app/service/snapshot.go +++ b/backend/app/service/snapshot.go @@ -2,8 +2,13 @@ package service import ( "context" + "encoding/json" "fmt" + "io/ioutil" "os" + "os/exec" + "path/filepath" + "strings" "time" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -21,7 +26,12 @@ type SnapshotService struct{} type ISnapshotService interface { SearchWithPage(req dto.PageInfo) (int64, interface{}, error) - Create(req dto.SnapshotCreate) error + SnapshotCreate(req dto.SnapshotCreate) error + SnapshotRecover(req dto.SnapshotRecover) error + SnapshotRollback(req dto.SnapshotRecover) error + Delete(req dto.BatchDeleteReq) error + + readFromJson(path string) (SnapshotJson, error) } func NewISnapshotService() ISnapshotService { @@ -29,11 +39,11 @@ func NewISnapshotService() ISnapshotService { } func (u *SnapshotService) SearchWithPage(req dto.PageInfo) (int64, interface{}, error) { - total, snapshots, err := snapshotRepo.Page(req.Page, req.PageSize) + total, systemBackups, err := snapshotRepo.Page(req.Page, req.PageSize) var dtoSnap []dto.SnapshotInfo - for _, snapshot := range snapshots { + for _, systemBackup := range systemBackups { var item dto.SnapshotInfo - if err := copier.Copy(&item, &snapshot); err != nil { + if err := copier.Copy(&item, &systemBackup); err != nil { return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) } dtoSnap = append(dtoSnap, item) @@ -41,12 +51,24 @@ func (u *SnapshotService) SearchWithPage(req dto.PageInfo) (int64, interface{}, return total, dtoSnap, err } -func (u *SnapshotService) Create(req dto.SnapshotCreate) error { +type SnapshotJson struct { + OldDockerDataDir string `json:"oldDockerDataDir"` + OldBackupDataDir string `json:"oldDackupDataDir"` + OldPanelDataDir string `json:"oldPanelDataDir"` + + DockerDataDir string `json:"dockerDataDir"` + BackupDataDir string `json:"backupDataDir"` + PanelDataDir string `json:"panelDataDir"` + LiveRestoreEnabled bool `json:"liveRestoreEnabled"` +} + +func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { + global.LOG.Info("start to create snapshot now") localDir, err := loadLocalDir() if err != nil { return err } - backup, err := backupRepo.Get(commonRepo.WithByType(req.BackupType)) + backup, err := backupRepo.Get(commonRepo.WithByType(req.From)) if err != nil { return err } @@ -56,95 +78,755 @@ func (u *SnapshotService) Create(req dto.SnapshotCreate) error { } timeNow := time.Now().Format("20060102150405") - rootDir := fmt.Sprintf("/tmp/songliu/1panel_backup_%s", timeNow) + rootDir := fmt.Sprintf("%s/system/1panel_snapshot_%s", localDir, timeNow) backupPanelDir := fmt.Sprintf("%s/1panel", rootDir) _ = os.MkdirAll(backupPanelDir, os.ModePerm) backupDockerDir := fmt.Sprintf("%s/docker", rootDir) _ = os.MkdirAll(backupDockerDir, os.ModePerm) - defer func() { - _, _ = cmd.Exec("systemctl start docker") - _ = os.RemoveAll(rootDir) - }() - - fileOp := files.NewFileOp() - if err := fileOp.Compress([]string{localDir}, backupPanelDir, "1panel_backup.tar.gz", files.TarGz); err != nil { - global.LOG.Errorf("snapshot backup 1panel backup datas %s failed, err: %v", localDir, err) - return err - } - client, err := docker.NewDockerClient() - if err != nil { - return err - } - ctx := context.Background() - info, err := client.Info(ctx) - if err != nil { - return err - } - dataDir := info.DockerRootDir - stdout, err := cmd.Exec("systemctl stop docker") - if err != nil { - return errors.New(stdout) - } - - if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil { - if err := fileOp.Compress([]string{dataDir}, backupDockerDir, "docker_data.tar.gz", files.TarGz); err != nil { - global.LOG.Errorf("snapshot backup docker data dir %s failed, err: %v", dataDir, err) - return err - } - } - if _, err := os.Stat(constant.DaemonJsonPath); err == nil { - if err := fileOp.CopyFile(constant.DaemonJsonPath, backupDockerDir); err != nil { - global.LOG.Errorf("snapshot backup daemon.json failed, err: %v", err) - return err - } - } - - if _, err := os.Stat("/Users/slooop/go/bin/swag"); err == nil { - if err := fileOp.CopyFile("/Users/slooop/go/bin/swag", backupPanelDir); err != nil { - global.LOG.Errorf("snapshot backup 1panel failed, err: %v", err) - return err - } - } - if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil { - if err := fileOp.CopyFile("/etc/systemd/system/1panel.service", backupPanelDir); err != nil { - global.LOG.Errorf("snapshot backup 1panel.service failed, err: %v", err) - return err - } - } - if _, err := os.Stat("/usr/local/bin/1panelctl"); err == nil { - if err := fileOp.CopyFile("/usr/local/bin/1panelctl", backupPanelDir); err != nil { - global.LOG.Errorf("snapshot backup 1panelctl failed, err: %v", err) - return err - } - } - if _, err := os.Stat(global.CONF.System.DataDir); err == nil { - if err := fileOp.Compress([]string{global.CONF.System.DataDir}, backupPanelDir, "1panel_data.tar.gz", files.TarGz); err != nil { - global.LOG.Errorf("snapshot backup 1panel data %s failed, err: %v", global.CONF.System.DataDir, err) - return err - } - } - if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_backup_%s.tar.gz", timeNow), files.TarGz); err != nil { - return err - } - + versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion")) snap := model.Snapshot{ - Name: "1panel_backup_" + timeNow, + Name: "1panel_snapshot_" + timeNow, Description: req.Description, - BackupType: req.BackupType, + From: req.From, + Version: versionItem.Value, Status: constant.StatusWaiting, } _ = snapshotRepo.Create(&snap) go func() { - localPath := fmt.Sprintf("%s/system/1panel_backup_%s.tar.gz", localDir, timeNow) - if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_backup_%s.tar.gz", timeNow)); err != nil || !ok { + defer func() { + global.LOG.Info("zhengque zoudao le zheli") + _ = os.RemoveAll(rootDir) + }() + fileOp := files.NewFileOp() + + dockerDataDir, liveRestoreStatus, err := u.loadDockerDataDir() + if err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + _, _ = cmd.Exec("systemctl stop docker") + if err := u.handleDockerDatas(fileOp, "snapshot", dockerDataDir, backupDockerDir); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + if err := u.handleDaemonJson(fileOp, "snapshot", "", backupDockerDir); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + _, _ = cmd.Exec("systemctl restart docker") + + if err := u.handlePanelBinary(fileOp, "snapshot", "", backupPanelDir+"/1panel"); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + if err := u.handlePanelctlBinary(fileOp, "snapshot", "", backupPanelDir+"/1pctl"); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + if err := u.handlePanelService(fileOp, "snapshot", "", backupPanelDir+"/1panel.service"); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + + if err := u.handleBackupDatas(fileOp, "snapshot", localDir, backupPanelDir); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + + if err := u.handlePanelDatas(fileOp, "snapshot", global.CONF.BaseDir+"/1Panel", backupPanelDir, localDir, dockerDataDir); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + + snapJson := SnapshotJson{DockerDataDir: dockerDataDir, BackupDataDir: localDir, PanelDataDir: global.CONF.BaseDir + "/1Panel", LiveRestoreEnabled: liveRestoreStatus} + if err := u.saveJson(snapJson, rootDir); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("save snapshot json failed, err: %v", err)) + return + } + + if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_snapshot_%s.tar.gz", timeNow), files.TarGz); err != nil { + updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error()) + return + } + global.LOG.Infof("start to upload snapshot to %s, please wait", backup.Type) + localPath := fmt.Sprintf("%s/system/1panel_snapshot_%s.tar.gz", localDir, timeNow) + if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_snapshot_%s.tar.gz", timeNow)); err != nil || !ok { _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err) return } - snap.Status = constant.StatusSuccess _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) + _ = os.RemoveAll(rootDir) + _ = os.RemoveAll(fmt.Sprintf("%s/system/1panel_snapshot_%s.tar.gz", localDir, timeNow)) + + updateSnapshotStatus(snap.ID, constant.StatusSuccess, "") global.LOG.Infof("upload snapshot to %s success", backup.Type) }() return nil } + +func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error { + global.LOG.Info("start to recvover panel by snapshot now") + snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if !req.IsNew && len(snap.InterruptStep) != 0 && len(snap.RollbackStatus) != 0 { + return fmt.Errorf("the snapshot has been rolled back and cannot be restored again") + } + isReTry := false + if len(snap.InterruptStep) != 0 && !req.IsNew { + isReTry = true + } + backup, err := backupRepo.Get(commonRepo.WithByType(snap.From)) + if err != nil { + return err + } + client, err := NewIBackupService().NewClient(&backup) + if err != nil { + return err + } + localDir, err := loadLocalDir() + if err != nil { + return err + } + baseDir := fmt.Sprintf("%s/system/%s", localDir, snap.Name) + if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { + _ = os.MkdirAll(baseDir, os.ModePerm) + } + + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting}) + go func() { + operation := "recover" + if isReTry { + operation = "re-recover" + } + if !isReTry || snap.InterruptStep == "Download" || (isReTry && req.ReDownload) { + ok, err := client.Download(fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name), fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name)) + if err != nil || !ok { + if req.ReDownload { + updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err)) + return + } + updateRecoverStatus(snap.ID, "Download", constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err)) + return + } + isReTry = false + } + fileOp := files.NewFileOp() + if !isReTry || snap.InterruptStep == "Decompress" || (isReTry && req.ReDownload) { + if err := fileOp.Decompress(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, files.TarGz); err != nil { + if req.ReDownload { + updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) + return + } + updateRecoverStatus(snap.ID, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) + return + } + isReTry = false + } + rootDir := fmt.Sprintf("%s/%s", baseDir, snap.Name) + originalDir := fmt.Sprintf("%s/original/", baseDir) + + snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir)) + if err != nil { + updateRecoverStatus(snap.ID, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) + return + } + if snap.InterruptStep == "Readjson" { + isReTry = false + } + + snapJson.OldPanelDataDir = global.CONF.BaseDir + "/1Panel" + snapJson.OldBackupDataDir = localDir + recoverPanelDir := fmt.Sprintf("%s/%s/1panel", baseDir, snap.Name) + liveRestore := false + if !isReTry || snap.InterruptStep == "LoadDockerJson" { + snapJson.OldDockerDataDir, liveRestore, err = u.loadDockerDataDir() + if err != nil { + updateRecoverStatus(snap.ID, "LoadDockerJson", constant.StatusFailed, fmt.Sprintf("load docker data dir failed, err: %v", err)) + return + } + isReTry = false + } + if liveRestore { + if err := u.updateLiveRestore(false); err != nil { + updateRecoverStatus(snap.ID, "UpdateLiveRestore", constant.StatusFailed, fmt.Sprintf("update docker daemon.json live-restore conf failed, err: %v", err)) + return + } + isReTry = false + } + _ = u.saveJson(snapJson, rootDir) + + _, _ = cmd.Exec("systemctl stop docker") + if !isReTry || snap.InterruptStep == "DockerDir" { + if err := u.handleDockerDatas(fileOp, operation, rootDir, snapJson.DockerDataDir); err != nil { + updateRecoverStatus(snap.ID, "DockerDir", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + if !isReTry || snap.InterruptStep == "DaemonJson" { + if err := u.handleDaemonJson(fileOp, operation, rootDir+"/docker/daemon.json", originalDir); err != nil { + updateRecoverStatus(snap.ID, "DaemonJson", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + _, _ = cmd.Exec("systemctl restart docker") + + if !isReTry || snap.InterruptStep == "1PanelBinary" { + if err := u.handlePanelBinary(fileOp, operation, recoverPanelDir+"/1panel", originalDir+"/1panel"); err != nil { + updateRecoverStatus(snap.ID, "1PanelBinary", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + if !isReTry || snap.InterruptStep == "1PctlBinary" { + if err := u.handlePanelctlBinary(fileOp, operation, recoverPanelDir+"/1pctl", originalDir+"/1pctl"); err != nil { + updateRecoverStatus(snap.ID, "1PctlBinary", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + if !isReTry || snap.InterruptStep == "1PanelService" { + if err := u.handlePanelService(fileOp, operation, recoverPanelDir+"/1panel.service", originalDir+"/1panel.service"); err != nil { + updateRecoverStatus(snap.ID, "1PanelService", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + + if !isReTry || snap.InterruptStep == "1PanelBackups" { + if err := u.handleBackupDatas(fileOp, operation, rootDir, snapJson.BackupDataDir); err != nil { + updateRecoverStatus(snap.ID, "1PanelBackups", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + + if !isReTry || snap.InterruptStep == "1PanelData" { + if err := u.handlePanelDatas(fileOp, operation, rootDir, snapJson.PanelDataDir, "", ""); err != nil { + updateRecoverStatus(snap.ID, "1PanelData", constant.StatusFailed, err.Error()) + return + } + isReTry = false + } + fmt.Println(000) + _ = os.RemoveAll(rootDir) + fmt.Println(111) + global.LOG.Info("recover successful") + fmt.Println(222) + _, _ = cmd.Exec("systemctl daemon-reload") + fmt.Println(333) + _, _ = cmd.Exec("systemctl restart 1panel.service") + fmt.Println(444) + updateRecoverStatus(snap.ID, "", constant.StatusSuccess, "") + fmt.Println(555) + }() + return nil +} + +func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error { + global.LOG.Info("start to rollback now") + snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if snap.InterruptStep == "Download" || snap.InterruptStep == "Decompress" || snap.InterruptStep == "Readjson" { + return nil + } + localDir, err := loadLocalDir() + if err != nil { + return err + } + fileOp := files.NewFileOp() + + rootDir := fmt.Sprintf("%s/system/%s/%s", localDir, snap.Name, snap.Name) + originalDir := fmt.Sprintf("%s/system/%s/original", localDir, snap.Name) + if _, err := os.Stat(originalDir); err != nil && os.IsNotExist(err) { + return fmt.Errorf("load original dir failed, err: %s", err) + } + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"rollback_status": constant.StatusWaiting}) + snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir)) + if err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) + return err + } + + _, _ = cmd.Exec("systemctl stop docker") + if err := u.handleDockerDatas(fileOp, "rollback", originalDir, snapJson.OldDockerDataDir); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "DockerDir" { + _, _ = cmd.Exec("systemctl restart docker") + return nil + } + + if err := u.handleDaemonJson(fileOp, "rollback", originalDir+"/daemon.json", ""); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "DaemonJson" { + _, _ = cmd.Exec("systemctl restart docker") + return nil + } + if snapJson.LiveRestoreEnabled { + if err := u.updateLiveRestore(true); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + } + if snap.InterruptStep == "UpdateLiveRestore" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + if err := u.handlePanelBinary(fileOp, "rollback", originalDir+"/1panel", ""); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "1PanelBinary" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + if err := u.handlePanelctlBinary(fileOp, "rollback", originalDir+"/1pctl", ""); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "1PctlBinary" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + if err := u.handlePanelService(fileOp, "rollback", originalDir+"/1panel.service", ""); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "1PanelService" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + if err := u.handleBackupDatas(fileOp, "rollback", originalDir, snapJson.OldBackupDataDir); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "1PanelBackups" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + if err := u.handlePanelDatas(fileOp, "rollback", originalDir, snapJson.OldPanelDataDir, "", ""); err != nil { + updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error()) + return err + } + if snap.InterruptStep == "1PanelData" { + _, _ = cmd.Exec("systemctl daemon-reload") + _, _ = cmd.Exec("systemctl restart 1panel.service") + return nil + } + + fmt.Println(000) + _ = os.RemoveAll(rootDir) + fmt.Println(111) + global.LOG.Info("rollback successful") + fmt.Println(222) + _, _ = cmd.Exec("systemctl daemon-reload") + fmt.Println(333) + _, _ = cmd.Exec("systemctl restart 1panel.service") + fmt.Println(444) + updateRollbackStatus(snap.ID, constant.StatusSuccess, "") + fmt.Println(555) + return nil +} + +func (u *SnapshotService) saveJson(snapJson SnapshotJson, path string) error { + remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") + if err := ioutil.WriteFile(fmt.Sprintf("%s/snapshot.json", path), remarkInfo, 0640); err != nil { + return err + } + return nil +} + +func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) { + var snap SnapshotJson + if _, err := os.Stat(path); err != nil { + return snap, fmt.Errorf("find snapshot json file in recover package failed, err: %v", err) + } + fileByte, err := os.ReadFile(path) + if err != nil { + return snap, fmt.Errorf("read file from path %s failed, err: %v", path, err) + } + if err := json.Unmarshal(fileByte, &snap); err != nil { + return snap, fmt.Errorf("unmarshal snapjson failed, err: %v", err) + } + return snap, nil +} + +func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error { + switch operation { + case "snapshot": + if err := u.handleTar(source, target, "docker_data.tar.gz", ""); err != nil { + return fmt.Errorf("backup docker data failed, err: %v", err) + } + case "recover": + if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "docker_data.tar.gz", ""); err != nil { + return fmt.Errorf("backup docker data failed, err: %v", err) + } + if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil { + return fmt.Errorf("recover docker data failed, err: %v", err) + } + case "re-recover": + if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil { + return fmt.Errorf("re-recover docker data failed, err: %v", err) + } + case "rollback": + if err := u.handleUnTar(source+"/docker_data.tar.gz", target); err != nil { + return fmt.Errorf("rollback docker data failed, err: %v", err) + } + } + global.LOG.Info("handle docker data dir successful!") + return nil +} + +func (u *SnapshotService) handleDaemonJson(fileOp files.FileOp, operation string, source, target string) error { + daemonJsonPath := "/etc/docker/daemon.json" + if operation == "snapshot" || operation == "recover" { + _, err := os.Stat(daemonJsonPath) + if os.IsNotExist(err) { + global.LOG.Info("no daemon.josn in snapshot and system now, nothing happened") + } + if err == nil { + if err := fileOp.CopyFile(daemonJsonPath, target); err != nil { + return fmt.Errorf("backup docker daemon.json failed, err: %v", err) + } + } + } + if operation == "recover" || operation == "rollback" || operation == "re-recover" { + _, sourceErr := os.Stat(source) + if os.IsNotExist(sourceErr) { + _ = os.Remove(daemonJsonPath) + } + if sourceErr == nil { + if err := fileOp.CopyFile(source, "/etc/docker"); err != nil { + return fmt.Errorf("recover docker daemon.json failed, err: %v", err) + } + } + } + global.LOG.Info("handle docker daemon.json successful!") + return nil +} + +func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation string, source, target string) error { + panelPath := "/usr/local/bin/1panel" + if operation == "snapshot" || operation == "recover" { + if _, err := os.Stat(panelPath); err != nil { + return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err) + } else { + if err := cpBinary(panelPath, target); err != nil { + return fmt.Errorf("backup 1panel binary failed, err: %v", err) + } + } + } + if operation == "recover" || operation == "rollback" || operation == "re-recover" { + if _, err := os.Stat(source); err != nil { + return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err) + } else { + if err := cpBinary(source, "/usr/local/bin/1panel"); err != nil { + return fmt.Errorf("recover 1panel binary failed, err: %v", err) + } + } + } + global.LOG.Info("handle binary panel successful!") + return nil +} +func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation string, source, target string) error { + panelctlPath := "/usr/local/bin/1pctl" + if operation == "snapshot" || operation == "recover" { + if _, err := os.Stat(panelctlPath); err != nil { + return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err) + } else { + if err := cpBinary(panelctlPath, target); err != nil { + return fmt.Errorf("backup 1pctl binary failed, err: %v", err) + } + } + } + if operation == "recover" || operation == "rollback" || operation == "re-recover" { + if _, err := os.Stat(source); err != nil { + return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err) + } else { + if err := cpBinary(source, "/usr/local/bin/1pctl"); err != nil { + return fmt.Errorf("recover 1pctl binary failed, err: %v", err) + } + } + } + global.LOG.Info("handle binary 1pactl successful!") + return nil +} + +func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation string, source, target string) error { + panelServicePath := "/etc/systemd/system/1panel.service" + if operation == "snapshot" || operation == "recover" { + if _, err := os.Stat(panelServicePath); err != nil { + return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err) + } else { + if err := cpBinary(panelServicePath, target); err != nil { + return fmt.Errorf("backup 1panel service failed, err: %v", err) + } + } + } + if operation == "recover" || operation == "rollback" || operation == "re-recover" { + if _, err := os.Stat(source); err != nil { + return fmt.Errorf("1panel service is not found in snapshot, err: %v", err) + } else { + if err := cpBinary(source, "/etc/systemd/system/1panel.service"); err != nil { + return fmt.Errorf("recover 1panel service failed, err: %v", err) + } + } + } + global.LOG.Info("handle panel service successful!") + return nil +} + +func (u *SnapshotService) handleBackupDatas(fileOp files.FileOp, operation string, source, target string) error { + switch operation { + case "snapshot": + if err := u.handleTar(source, target, "1panel_backup.tar.gz", "./system"); err != nil { + return fmt.Errorf("backup panel local backup dir data failed, err: %v", err) + } + case "recover": + if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "1panel_backup.tar.gz", "./system"); err != nil { + return fmt.Errorf("restore original local backup dir data failed, err: %v", err) + } + if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil { + return fmt.Errorf("recover local backup dir data failed, err: %v", err) + } + case "re-recover": + if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil { + return fmt.Errorf("retry recover local backup dir data failed, err: %v", err) + } + case "rollback": + if err := u.handleUnTar(source+"/1panel_backup.tar.gz", target); err != nil { + return fmt.Errorf("rollback local backup dir data failed, err: %v", err) + } + } + global.LOG.Info("handle backup data successful!") + return nil +} + +func (u *SnapshotService) handlePanelDatas(fileOp files.FileOp, operation string, source, target, backupDir, dockerDir string) error { + switch operation { + case "snapshot": + exclusionRules := "" + if strings.Contains(backupDir, source) { + exclusionRules += ("." + strings.ReplaceAll(backupDir, source, "") + ";") + } + if strings.Contains(dockerDir, source) { + exclusionRules += ("." + strings.ReplaceAll(dockerDir, source, "") + ";") + } + if err := u.handleTar(source, target, "1panel_data.tar.gz", exclusionRules); err != nil { + return fmt.Errorf("backup panel data failed, err: %v", err) + } + case "recover": + exclusionRules := "" + if strings.Contains(backupDir, target) { + exclusionRules += ("1Panel" + strings.ReplaceAll(backupDir, target, "") + ";") + } + if strings.Contains(dockerDir, target) { + exclusionRules += ("1Panel" + strings.ReplaceAll(dockerDir, target, "") + ";") + } + if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "1panel_data.tar.gz", exclusionRules); err != nil { + return fmt.Errorf("restore original panel data failed, err: %v", err) + } + + if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil { + return fmt.Errorf("recover panel data failed, err: %v", err) + } + case "re-recover": + if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil { + return fmt.Errorf("retry recover panel data failed, err: %v", err) + } + case "rollback": + if err := u.handleUnTar(source+"/1panel_data.tar.gz", target); err != nil { + return fmt.Errorf("rollback panel data failed, err: %v", err) + } + } + + global.LOG.Info("handle panel data successful!") + return nil +} + +func (u *SnapshotService) loadDockerDataDir() (string, bool, error) { + client, err := docker.NewDockerClient() + if err != nil { + return "", false, fmt.Errorf("new docker client failed, err: %v", err) + } + info, err := client.Info(context.Background()) + if err != nil { + return "", false, fmt.Errorf("load docker info failed, err: %v", err) + } + return info.DockerRootDir, info.LiveRestoreEnabled, nil +} + +func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error { + backups, _ := snapshotRepo.GetList(commonRepo.WithIdsIn(req.Ids)) + localDir, err := loadLocalDir() + if err != nil { + return err + } + for _, snap := range backups { + if _, err := os.Stat(fmt.Sprintf("%s/system/%s/%s.tar.gz", localDir, snap.Name, snap.Name)); err == nil { + _ = os.Remove(fmt.Sprintf("%s/system/%s/%s.tar.gz", localDir, snap.Name, snap.Name)) + } + } + if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { + return err + } + + return nil +} + +func updateSnapshotStatus(id uint, status string, message string) { + if status != constant.StatusSuccess { + global.LOG.Errorf("snapshot failed, err: %s", message) + } + if err := snapshotRepo.Update(id, map[string]interface{}{ + "status": status, + "message": message, + }); err != nil { + global.LOG.Errorf("update snap snapshot status failed, err: %v", err) + } +} +func updateRecoverStatus(id uint, interruptStep, status string, 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("2006-01-02 15:04:05"), + }); err != nil { + global.LOG.Errorf("update snap recover status failed, err: %v", err) + } +} +func updateRollbackStatus(id uint, status string, message string) { + 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("2006-01-02 15:04:05"), + }); 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("2006-01-02 15:04:05"), + }); err != nil { + global.LOG.Errorf("update snap recover status failed, err: %v", err) + } +} + +func cpBinary(src, dst string) error { + stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", src, dst)) + if err != nil { + return errors.New(stderr) + } + return nil +} + +func (u *SnapshotService) updateLiveRestore(enabled bool) error { + if _, err := os.Stat(constant.DaemonJsonPath); err != nil { + return fmt.Errorf("load docker daemon.json conf failed, err: %v", err) + } + file, err := ioutil.ReadFile(constant.DaemonJsonPath) + if err != nil { + return err + } + deamonMap := make(map[string]interface{}) + _ = json.Unmarshal(file, &deamonMap) + + if !enabled { + delete(deamonMap, "live-restore") + } else { + deamonMap["live-restore"] = enabled + } + newJson, err := json.MarshalIndent(deamonMap, "", "\t") + if err != nil { + return err + } + if err := ioutil.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { + return err + } + + stdout, err := cmd.Exec("systemctl restart docker") + if err != nil { + return errors.New(stdout) + } + time.Sleep(10 * time.Second) + return nil +} + +func (u *SnapshotService) handleTar(sourceDir, targetDir, name, exclusionRules string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + exStr := []string{"--warning=no-file-changed"} + exStr = append(exStr, "-zcf") + exStr = append(exStr, targetDir+"/"+name) + excludes := strings.Split(exclusionRules, ";") + for _, exclude := range excludes { + if len(exclude) == 0 { + continue + } + exStr = append(exStr, "--exclude") + exStr = append(exStr, exclude) + } + exStr = append(exStr, "-C") + exStr = append(exStr, sourceDir) + exStr = append(exStr, ".") + cmd := exec.Command("tar", exStr...) + stdout, err := cmd.CombinedOutput() + if err != nil { + return errors.New(string(stdout)) + } + return nil +} + +func (u *SnapshotService) handleUnTar(sourceDir, targetDir string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + exStr := []string{} + exStr = append(exStr, "zxf") + exStr = append(exStr, sourceDir) + exStr = append(exStr, "-C") + exStr = append(exStr, targetDir) + exStr = append(exStr, ".") + cmd := exec.Command("tar", exStr...) + stdout, err := cmd.CombinedOutput() + if err != nil { + return errors.New(string(stdout)) + } + return nil +} diff --git a/backend/app/service/snapshot_test.go b/backend/app/service/snapshot_test.go index 91549ba6..0d8a61d2 100644 --- a/backend/app/service/snapshot_test.go +++ b/backend/app/service/snapshot_test.go @@ -1,34 +1,60 @@ package service import ( + "context" "fmt" + "io/ioutil" + "strings" "testing" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/init/db" "github.com/1Panel-dev/1Panel/backend/init/viper" - "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/google/go-github/github" ) -func TestSnaa(t *testing.T) { - fileOp := files.NewFileOp() - - fmt.Println(fileOp.CopyFile("/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads")) - // fmt.Println(fileOp.Compress([]string{"/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads/1Panel.db"}, "/Users/slooop/Downloads/", "test.tar.gz", files.TarGz)) -} - -func TestOss(t *testing.T) { +func TestDw(t *testing.T) { viper.Init() db.Init() - var backup model.BackupAccount - if err := global.DB.Where("id = ?", 6).First(&backup).Error; err != nil { - fmt.Println(err) - } - backupAccont, err := NewIBackupService().NewClient(&backup) + backup, err := backupRepo.Get(commonRepo.WithByType("OSS")) if err != nil { fmt.Println(err) } - fmt.Println(backupAccont.Upload("/Users/slooop/Downloads/1Panel.db", "database/1Panel.db")) + client, err := NewIBackupService().NewClient(&backup) + if err != nil { + fmt.Println(err) + } + fmt.Println(client.Download("system_snapshot/1panel_snapshot_20230112135640.tar.gz", "/opt/1Panel/data/backup/system/test.tar.gz")) +} + +func TestDi(t *testing.T) { + docker := "var/lib/docker" + fmt.Println(docker[strings.LastIndex(docker, "/"):]) + fmt.Println(docker[:strings.LastIndex(docker, "/")]) +} + +func TestGit(t *testing.T) { + client := github.NewClient(nil) + stats, _, err := client.Repositories.GetLatestRelease(context.Background(), "KubeOperator", "KubeOperator") + fmt.Println(github.Timestamp(*stats.PublishedAt), err) +} + +func TestSdasd(t *testing.T) { + u := NewISnapshotService() + var snapjson SnapshotJson + snapjson, _ = u.readFromJson("/Users/slooop/Downloads/snapshot.json") + fmt.Println(111, snapjson) + // if err := ioutil.WriteFile("/Users/slooop/Downloads/snapshot.json", []byte("111xxxxx"), 0640); err != nil { + // fmt.Println(err) + // } +} + +func TestCp(t *testing.T) { + _, err := ioutil.ReadFile("/Users/slooop/Downloads/test/main") + if err != nil { + fmt.Println(err) + } + if err := ioutil.WriteFile("/Users/slooop/Downloads/test/main", []byte("sdadasd"), 0640); err != nil { + fmt.Println(err) + } } diff --git a/backend/configs/cache.go b/backend/configs/cache.go deleted file mode 100644 index 6222464e..00000000 --- a/backend/configs/cache.go +++ /dev/null @@ -1,5 +0,0 @@ -package configs - -type Cache struct { - Path string `mapstructure:"path"` -} diff --git a/backend/configs/config.go b/backend/configs/config.go index a38c7197..e76d3829 100644 --- a/backend/configs/config.go +++ b/backend/configs/config.go @@ -1,11 +1,10 @@ package configs type ServerConfig struct { - Sqlite Sqlite `mapstructure:"sqlite"` + BaseDir string `mapstructure:"base_dir"` System System `mapstructure:"system"` + Sqlite Sqlite `mapstructure:"sqlite"` LogConfig LogConfig `mapstructure:"log"` CORS CORS `mapstructure:"cors"` Encrypt Encrypt `mapstructure:"encrypt"` - Csrf Csrf `mapstructure:"csrf"` - Cache Cache `mapstructure:"cache"` } diff --git a/backend/configs/csrf.go b/backend/configs/csrf.go deleted file mode 100644 index 11d3f930..00000000 --- a/backend/configs/csrf.go +++ /dev/null @@ -1,5 +0,0 @@ -package configs - -type Csrf struct { - Key string `mapstructure:"key" json:"key" yaml:"key"` -} diff --git a/backend/configs/system.go b/backend/configs/system.go index 076efa41..2a4b655d 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -3,7 +3,8 @@ package configs type System struct { Port int `mapstructure:"port"` DbType string `mapstructure:"db_type"` - Level string `mapstructure:"level"` DataDir string `mapstructure:"data_dir"` + Cache string `mapstructure:"cache"` + Backup string `mapstructure:"backup"` AppOss string `mapstructure:"app_oss"` } diff --git a/backend/constant/container.go b/backend/constant/container.go index a843c8fa..a55074f0 100644 --- a/backend/constant/container.go +++ b/backend/constant/container.go @@ -16,5 +16,5 @@ const ( TmpDockerBuildDir = "/opt/1Panel/data/docker/build" TmpComposeBuildDir = "/opt/1Panel/data/docker/compose" - DaemonJsonPath = "/tmp/docker/daemon.json" + DaemonJsonPath = "/etc/docker/daemon.json" ) diff --git a/backend/init/cache/cache.go b/backend/init/cache/cache.go index 54d04519..7a68f663 100644 --- a/backend/init/cache/cache.go +++ b/backend/init/cache/cache.go @@ -1,18 +1,19 @@ package cache import ( + "time" + "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/init/cache/badger_db" "github.com/dgraph-io/badger/v3" - "time" ) func Init() { - c := global.CONF.Cache + c := global.CONF.System.Cache options := badger.Options{ - Dir: c.Path, - ValueDir: c.Path, + Dir: c, + ValueDir: c, ValueLogFileSize: 102400000, ValueLogMaxEntries: 100000, VLogPercentile: 0.1, diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 4bb9bca7..453e6b1d 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -19,7 +19,7 @@ func Init() { migrations.AddTableImageRepo, migrations.AddTableWebsite, migrations.AddTableDatabaseMysql, - migrations.AddTableSnapshot, + migrations.AddTableSnap, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index 94c12ed1..412c575a 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -126,6 +126,9 @@ var AddTableSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "DingVars", Value: ""}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: "v1.0.0"}).Error; err != nil { + return err + } return nil }, } @@ -203,8 +206,8 @@ var AddTableWebsite = &gormigrate.Migration{ }, } -var AddTableSnapshot = &gormigrate.Migration{ - ID: "20230106-add-table-snapshot", +var AddTableSnap = &gormigrate.Migration{ + ID: "20230106-add-table-snap", Migrate: func(tx *gorm.DB) error { if err := tx.AutoMigrate(&model.Snapshot{}); err != nil { return err diff --git a/backend/middleware/csrf.go b/backend/middleware/csrf.go deleted file mode 100644 index 28c32ab0..00000000 --- a/backend/middleware/csrf.go +++ /dev/null @@ -1,29 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/gin-gonic/gin" - "github.com/gorilla/csrf" - adapter "github.com/gwatts/gin-adapter" -) - -func CSRF() gin.HandlerFunc { - csrfMd := csrf.Protect( - []byte(global.CONF.Csrf.Key), - csrf.Path("/api"), - csrf.ErrorHandler(http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("csrf token invalid")) - })), - ) - return adapter.Wrap(csrfMd) -} - -func LoadCsrfToken() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("X-CSRF-TOKEN", csrf.Token(c.Request)) - } -} diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index fe8347b8..dcd5d91b 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -9,16 +9,15 @@ import ( type SettingRouter struct{} func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { - baseRouter := Router.Group("settings") settingRouter := Router.Group("settings"). Use(middleware.JwtAuth()). Use(middleware.SessionAuth()). Use(middleware.PasswordExpired()) baseApi := v1.ApiGroupApp.BaseApi { - baseRouter.POST("/search", baseApi.GetSettingInfo) - baseRouter.POST("/expired/handle", baseApi.HandlePasswordExpired) - baseRouter.POST("/update", baseApi.UpdateSetting) + settingRouter.POST("/search", baseApi.GetSettingInfo) + settingRouter.POST("/expired/handle", baseApi.HandlePasswordExpired) + settingRouter.POST("/update", baseApi.UpdateSetting) settingRouter.POST("/password/update", baseApi.UpdatePassword) settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) @@ -26,5 +25,9 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/mfa/bind", baseApi.MFABind) settingRouter.POST("/snapshot", baseApi.CreateSnapshot) settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot) + settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot) + settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot) + settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot) + settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) } } diff --git a/backend/server/server.go b/backend/server/server.go index b426b26e..01df9a3a 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -3,9 +3,10 @@ package server import ( "encoding/gob" "fmt" + "time" + "github.com/1Panel-dev/1Panel/backend/init/app" "github.com/1Panel-dev/1Panel/backend/init/business" - "time" "github.com/1Panel-dev/1Panel/backend/cron" "github.com/1Panel-dev/1Panel/backend/init/cache" @@ -34,7 +35,7 @@ func Start() { gob.Register(psession.SessionUser{}) cache.Init() session.Init() - gin.SetMode(global.CONF.System.Level) + gin.SetMode("debug") cron.Run() business.Init() diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index a62f534d..e135c85e 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -5976,28 +5976,6 @@ var doc = `{ } } }, - "/settings/daemonjson": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "加载 docker 配置路径", - "tags": [ - "System Setting" - ], - "summary": "Load daemon.json path", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "string" - } - } - } - } - }, "/settings/expired/handle": { "post": { "security": [ @@ -6188,6 +6166,238 @@ var doc = `{ } } }, + "/settings/snapshot": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Create system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotCreate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "name", + "description" + ], + "formatEN": "Create system backup [name][description]", + "formatZH": "创建系统快照 [name][description]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Delete system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "ids", + "isList": true, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete system backup [name]", + "formatZH": "删除系统快照 [name]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照恢复", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Recover system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "id", + "isList": false, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Recover from system backup [name]", + "formatZH": "从系统快照 [name] 恢复", + "paramKeys": [] + } + } + }, + "/settings/snapshot/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照回滚", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Rollback system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "id", + "isList": false, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Rollback from system backup [name]", + "formatZH": "从系统快照 [name] 回滚", + "paramKeys": [] + } + } + }, + "/settings/snapshot/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统快照列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Page system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, "/settings/time/sync": { "post": { "security": [ @@ -6260,6 +6470,28 @@ var doc = `{ } } }, + "/settings/upgrade": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载系统更新信息", + "tags": [ + "System Setting" + ], + "summary": "Load upgrade info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UpgradeInfo" + } + } + } + } + }, "/websites": { "post": { "security": [ @@ -8629,6 +8861,9 @@ var doc = `{ }, "status": { "type": "string" + }, + "version": { + "type": "string" } } }, @@ -9960,6 +10195,9 @@ var doc = `{ "sessionTimeout": { "type": "string" }, + "systemVersion": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9985,6 +10223,63 @@ var doc = `{ } } }, + "dto.SnapshotCreate": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "description": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO" + ] + } + } + }, + "dto.SnapshotRecover": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "isNew": { + "type": "boolean" + }, + "reDownload": { + "type": "boolean" + } + } + }, + "dto.UpgradeInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "publishedAt": { + "type": "string" + }, + "releaseNote": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "dto.UploadRecover": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 4d6882b6..168203e6 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -5962,28 +5962,6 @@ } } }, - "/settings/daemonjson": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "加载 docker 配置路径", - "tags": [ - "System Setting" - ], - "summary": "Load daemon.json path", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "string" - } - } - } - } - }, "/settings/expired/handle": { "post": { "security": [ @@ -6174,6 +6152,238 @@ } } }, + "/settings/snapshot": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Create system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotCreate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "name", + "description" + ], + "formatEN": "Create system backup [name][description]", + "formatZH": "创建系统快照 [name][description]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Delete system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "ids", + "isList": true, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete system backup [name]", + "formatZH": "删除系统快照 [name]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照恢复", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Recover system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "id", + "isList": false, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Recover from system backup [name]", + "formatZH": "从系统快照 [name] 恢复", + "paramKeys": [] + } + } + }, + "/settings/snapshot/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照回滚", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Rollback system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [ + { + "db": "snapshots", + "input_colume": "id", + "input_value": "id", + "isList": false, + "output_colume": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Rollback from system backup [name]", + "formatZH": "从系统快照 [name] 回滚", + "paramKeys": [] + } + } + }, + "/settings/snapshot/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统快照列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Page system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, "/settings/time/sync": { "post": { "security": [ @@ -6246,6 +6456,28 @@ } } }, + "/settings/upgrade": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载系统更新信息", + "tags": [ + "System Setting" + ], + "summary": "Load upgrade info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UpgradeInfo" + } + } + } + } + }, "/websites": { "post": { "security": [ @@ -8615,6 +8847,9 @@ }, "status": { "type": "string" + }, + "version": { + "type": "string" } } }, @@ -9946,6 +10181,9 @@ "sessionTimeout": { "type": "string" }, + "systemVersion": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9971,6 +10209,63 @@ } } }, + "dto.SnapshotCreate": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "description": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO" + ] + } + } + }, + "dto.SnapshotRecover": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "isNew": { + "type": "boolean" + }, + "reDownload": { + "type": "boolean" + } + } + }, + "dto.UpgradeInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "publishedAt": { + "type": "string" + }, + "releaseNote": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "dto.UploadRecover": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index a4808b9d..9c37f284 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -391,6 +391,8 @@ definitions: type: array status: type: string + version: + type: string type: object dto.DaemonJsonUpdateByFile: properties: @@ -1278,6 +1280,8 @@ definitions: type: string sessionTimeout: type: string + systemVersion: + type: string theme: type: string userName: @@ -1294,6 +1298,44 @@ definitions: required: - key type: object + dto.SnapshotCreate: + properties: + description: + type: string + from: + enum: + - OSS + - S3 + - SFTP + - MINIO + type: string + required: + - from + type: object + dto.SnapshotRecover: + properties: + id: + type: integer + isNew: + type: boolean + reDownload: + type: boolean + required: + - id + type: object + dto.UpgradeInfo: + properties: + createdAt: + type: string + newVersion: + type: string + publishedAt: + type: string + releaseNote: + type: string + tag: + type: string + type: object dto.UploadRecover: properties: dbName: @@ -6314,19 +6356,6 @@ paths: formatEN: Update nginx conf [domain] formatZH: 更新 nginx 配置 [domain] paramKeys: [] - /settings/daemonjson: - get: - description: 加载 docker 配置路径 - responses: - "200": - description: OK - schema: - type: string - security: - - ApiKeyAuth: [] - summary: Load daemon.json path - tags: - - System Setting /settings/expired/handle: post: consumes: @@ -6448,6 +6477,155 @@ paths: summary: Load system setting info tags: - System Setting + /settings/snapshot: + post: + consumes: + - application/json + description: 创建系统快照 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotCreate' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Create system backup + tags: + - System Setting + x-panel-log: + BeforeFuntions: [] + bodyKeys: + - name + - description + formatEN: Create system backup [name][description] + formatZH: 创建系统快照 [name][description] + paramKeys: [] + /settings/snapshot/del: + post: + consumes: + - application/json + description: 删除系统快照 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Delete system backup + tags: + - System Setting + x-panel-log: + BeforeFuntions: + - db: snapshots + input_colume: id + input_value: ids + isList: true + output_colume: name + output_value: name + bodyKeys: + - ids + formatEN: Delete system backup [name] + formatZH: 删除系统快照 [name] + paramKeys: [] + /settings/snapshot/recover: + post: + consumes: + - application/json + description: 从系统快照恢复 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotRecover' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Recover system backup + tags: + - System Setting + x-panel-log: + BeforeFuntions: + - db: snapshots + input_colume: id + input_value: id + isList: false + output_colume: name + output_value: name + bodyKeys: + - id + formatEN: Recover from system backup [name] + formatZH: 从系统快照 [name] 恢复 + paramKeys: [] + /settings/snapshot/rollback: + post: + consumes: + - application/json + description: 从系统快照回滚 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotRecover' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Rollback system backup + tags: + - System Setting + x-panel-log: + BeforeFuntions: + - db: snapshots + input_colume: id + input_value: id + isList: false + output_colume: name + output_value: name + bodyKeys: + - id + formatEN: Rollback from system backup [name] + formatZH: 从系统快照 [name] 回滚 + paramKeys: [] + /settings/snapshot/search: + post: + consumes: + - application/json + description: 获取系统快照列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page system snapshot + tags: + - System Setting /settings/time/sync: post: description: 系统时间同步 @@ -6495,6 +6673,19 @@ paths: formatEN: update system setting [key] => [value] formatZH: 修改系统配置 [key] => [value] paramKeys: [] + /settings/upgrade: + get: + description: 加载系统更新信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.UpgradeInfo' + security: + - ApiKeyAuth: [] + summary: Load upgrade info + tags: + - System Setting /websites: post: consumes: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd7fadf8..639ac05c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "fit2cloud-ui-plus": "^0.0.1-beta.15", "js-base64": "^3.7.2", "js-md5": "^0.7.3", + "md-editor-v3": "^2.7.2", "monaco-editor": "^0.34.0", "nprogress": "^0.2.0", "pinia": "^2.0.12", @@ -9027,6 +9028,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/md-editor-v3": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-2.7.2.tgz", + "integrity": "sha512-CyLG7yZhMyKplXO/MYIccpL0AOcnys74cMpbBG77rmXWlANAmzLrznUU++g6MohTv3DCRNTz+5Uh/w9h9P2sSA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -20493,6 +20502,11 @@ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", "dev": true }, + "md-editor-v3": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-2.7.2.tgz", + "integrity": "sha512-CyLG7yZhMyKplXO/MYIccpL0AOcnys74cMpbBG77rmXWlANAmzLrznUU++g6MohTv3DCRNTz+5Uh/w9h9P2sSA==" + }, "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8da20fed..dbb7ec76 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "fit2cloud-ui-plus": "^0.0.1-beta.15", "js-base64": "^3.7.2", "js-md5": "^0.7.3", + "md-editor-v3": "^2.7.2", "monaco-editor": "^0.34.0", "nprogress": "^0.2.0", "pinia": "^2.0.12", diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 4c342648..21ba5b17 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -5,6 +5,7 @@ export namespace Setting { userName: string; password: string; email: string; + systemVersion: string; sessionTimeout: number; localTime: string; @@ -46,16 +47,36 @@ export namespace Setting { code: string; } export interface SnapshotCreate { + from: string; description: string; - backupType: string; + } + export interface SnapshotRecover { + id: number; + isNew: boolean; + reDownload: boolean; } export interface SnapshotInfo { id: number; name: string; + from: string; description: string; - backupType: string; status: string; message: string; createdAt: DateTimeFormats; + version: string; + interruptStep: string; + recoverStatus: string; + recoverMessage: string; + lastRecoveredAt: string; + rollbackStatus: string; + rollbackMessage: string; + lastRollbackedAt: string; + } + export interface UpgradeInfo { + newVersion: string; + tag: string; + releaseNote: string; + createdAt: string; + publishedAt: string; } } diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index d5abdd11..ee4a07fb 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -42,6 +42,20 @@ export const bindMFA = (param: Setting.MFABind) => { export const snapshotCreate = (param: Setting.SnapshotCreate) => { return http.post(`/settings/snapshot`, param); }; +export const snapshotDelete = (param: { ids: number[] }) => { + return http.post(`/settings/snapshot/del`, param); +}; +export const snapshotRecover = (param: Setting.SnapshotRecover) => { + return http.post(`/settings/snapshot/recover`, param); +}; +export const snapshotRollback = (param: Setting.SnapshotRecover) => { + return http.post(`/settings/snapshot/rollback`, param); +}; export const searchSnapshotPage = (param: ReqPage) => { return http.post>(`/settings/snapshot/search`, param); }; + +// upgrade +export const loadUpgradeInfo = () => { + return http.get(`/settings/upgrade`); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f9f17ff3..33969cc3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -27,6 +27,7 @@ export default { log: 'Log', back: 'Back', recover: 'Recover', + retry: 'Retry', upload: 'Upload', download: 'Download', init: 'Init', @@ -51,6 +52,7 @@ export default { records: 'Records', group: 'Group', createdAt: 'Creation Time', + publishedAt: 'Publish Time', date: 'Date', updatedAt: 'Update Time', operate: 'Operations', @@ -737,6 +739,27 @@ export default { mfaHelper3: 'Enter six digits from the app', snapshot: 'Snapshot', + recoverDetail: 'Recover detail', + createSnapshot: 'Create snapshot', + recover: 'Recover', + noRecoverRecord: 'No recovery record has been recorded', + lastRecoverAt: 'Last recovery time', + lastRollbackAt: 'Last rollback time', + noRollbackRecord: 'No rollback record has been recorded', + reDownload: 'Download the backup file again', + recoverRecord: 'Recover record', + recoverHelper: + 'The recovery is about to start from snapshot {0}, and the recovery needs to restart docker and 1panel service, do you want to continue?', + rollback: 'Rollback', + rollbackHelper: + 'This recovery is about to be rolled back, which will replace all the files recovered this time. In the process, docker and 1panel services may need to be restarted. Do you want to continue?', + + upgrade: 'Upgrade', + newVersion: 'NewVersion', + upgradeCheck: 'Check for updates', + tag: 'Tag', + upgradeNotes: 'Release note', + upgradeNow: 'Upgrade now', enableMonitor: 'Enable', storeDays: 'Expiration time (day)', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 33150fc5..bcb523a3 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -28,6 +28,7 @@ export default { log: '日志', back: '返回', recover: '恢复', + retry: '重试', upload: '上传', download: '下载', init: '初始化', @@ -49,9 +50,11 @@ export default { status: '状态', statusSuccess: '成功', statusFailed: '失败', + statusWaiting: '进行中...', records: '任务输出', group: '分组', createdAt: '创建时间', + publishedAt: '发布时间', date: '时间', updatedAt: '更新时间', operate: '操作', @@ -726,6 +729,30 @@ export default { path: '路径', snapshot: '快照', + recoverDetail: '恢复详情', + createSnapshot: '新建快照', + recover: '恢复', + noRecoverRecord: '暂无恢复记录', + lastRecoverAt: '上次恢复时间', + lastRollbackAt: '上次回滚时间', + noRollbackRecord: '暂无回滚记录', + reDownload: '重新下载备份文件', + statusAll: '全部', + statusSuccess: '成功', + statusFailed: '失败', + versionChange: '版本变化', + snapshotFrom: '快照存储位置', + recoverHelper: '即将从快照 {0} 开始恢复,恢复需要重启 docker 以及 1panel 服务,是否继续?', + rollback: '回滚', + rollbackHelper: + '即将回滚本次恢复,回滚将替换所有本次恢复的文件,过程中可能需要重启 docker 以及 1panel 服务,是否继续?', + + upgrade: '升级', + newVersion: '新版本', + upgradeCheck: '检查更新', + tag: '标签', + upgradeNotes: '更新内容', + upgradeNow: '立即更新', safe: '安全', panelPort: '面板端口', diff --git a/frontend/src/views/setting/about/index.vue b/frontend/src/views/setting/about/index.vue index ceac8586..2a9fefb6 100644 --- a/frontend/src/views/setting/about/index.vue +++ b/frontend/src/views/setting/about/index.vue @@ -12,7 +12,12 @@

{{ $t('setting.description') }}

-

v1.0.0

+

+ {{ version }} + + {{ $t('setting.upgradeCheck') }} + +

@@ -33,11 +38,47 @@
+ + + + {{ upgradeInfo.newVersion }} + + + {{ upgradeInfo.tag }} + + + + + + {{ upgradeInfo.createdAt }} + + + {{ upgradeInfo.publishedAt }} + + + {{ $t('setting.upgradeNow') }} + + + diff --git a/frontend/src/views/setting/index.vue b/frontend/src/views/setting/index.vue index 53d6b298..90584509 100644 --- a/frontend/src/views/setting/index.vue +++ b/frontend/src/views/setting/index.vue @@ -58,6 +58,7 @@ const handleChange = (val: string) => { break; case 'snapshot': routerTo('/setting/snapshot'); + break; } }; diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index 1cb31ec9..799259d2 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -127,8 +127,6 @@ import i18n from '@/lang'; import { Rules } from '@/global/form-rules'; import { dateFromat } from '@/utils/util'; -const emit = defineEmits(['search']); - const loading = ref(false); const form = reactive({ serverPort: '', @@ -207,7 +205,7 @@ const handleMFA = async () => { await updateSetting({ key: 'MFAStatus', value: 'disable' }) .then(() => { loading.value = false; - emit('search'); + search(); ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); }) .catch(() => { @@ -221,7 +219,7 @@ const onBind = async () => { await bindMFA({ code: mfaCode.value, secret: otp.secret }) .then(() => { loading.value = false; - emit('search'); + search(); ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); isMFAShow.value = false; }) @@ -249,7 +247,7 @@ const submitTimeout = async (formEl: FormInstance | undefined) => { await updateSetting({ key: 'ExpirationDays', value: timeoutForm.days + '' }) .then(() => { loading.value = false; - emit('search'); + search(); loadTimeOut(); form.expirationTime = dateFromat(0, 0, time); timeoutVisiable.value = false; diff --git a/frontend/src/views/setting/snapshot/index.vue b/frontend/src/views/setting/snapshot/index.vue index 789e1057..c3267a07 100644 --- a/frontend/src/views/setting/snapshot/index.vue +++ b/frontend/src/views/setting/snapshot/index.vue @@ -1,42 +1,70 @@