From e7af9de9eddebca7c67e615bef18e4a7c4db4125 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Mon, 9 Jan 2023 22:55:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BB=8E=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E6=81=A2=E5=A4=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 增加升级备份与恢复 --- backend/app.yaml | 31 +- backend/app/api/v1/snapshot.go | 89 +- backend/app/api/v1/upgrade.go | 34 + backend/app/dto/setting.go | 31 +- backend/app/model/cronjob.go | 2 +- backend/app/model/snapshot.go | 10 +- backend/app/repo/common.go | 15 + backend/app/repo/cronjob.go | 11 - backend/app/repo/snapshot.go | 25 +- backend/app/service/cornjob.go | 2 +- backend/app/service/cronjob_helper.go | 1 + backend/app/service/snapshot.go | 798 +- backend/app/service/snapshot_test.go | 56 +- backend/configs/cache.go | 5 - backend/configs/config.go | 5 +- backend/configs/csrf.go | 5 - backend/configs/system.go | 3 +- backend/constant/container.go | 2 +- backend/init/cache/cache.go | 9 +- backend/init/migration/migrate.go | 2 +- backend/init/migration/migrations/init.go | 7 +- backend/middleware/csrf.go | 29 - backend/router/ro_setting.go | 11 +- backend/server/server.go | 5 +- cmd/server/docs/docs.go | 339 +- cmd/server/docs/swagger.json | 339 +- cmd/server/docs/swagger.yaml | 217 +- frontend/package-lock.json | 14 + frontend/package.json | 1 + frontend/src/api/interface/setting.ts | 25 +- frontend/src/api/modules/setting.ts | 14 + frontend/src/lang/modules/en.ts | 23 + frontend/src/lang/modules/zh.ts | 27 + frontend/src/views/setting/about/index.vue | 53 +- frontend/src/views/setting/index.vue | 1 + frontend/src/views/setting/safe/index.vue | 8 +- frontend/src/views/setting/snapshot/index.vue | 137 +- .../views/setting/snapshot/status/index.vue | 242 + go.mod | 4 +- go.sum | 8 +- node_modules/.package-lock.json | 15 + node_modules/md-editor-v3/LICENSE | 21 + node_modules/md-editor-v3/README.md | 779 ++ .../md-editor-v3/lib/MdEditor/Editor.d.ts | 356 + .../md-editor-v3/lib/MdEditor/config.d.ts | 26 + .../MdEditor/extensions/DropdownToolbar.d.ts | 39 + .../extensions/MdCatalog/CatalogLink.d.ts | 77 + .../MdEditor/extensions/MdCatalog/index.d.ts | 111 + .../lib/MdEditor/extensions/ModalToolbar.d.ts | 96 + .../MdEditor/extensions/NormalToolbar.d.ts | 27 + .../md-editor-v3/lib/MdEditor/index.d.ts | 811 ++ .../md-editor-v3/lib/MdEditor/type.d.ts | 328 + .../md-editor-v3/lib/md-editor-v3.es.js | 7079 +++++++++++++++++ .../md-editor-v3/lib/md-editor-v3.umd.js | 126 + node_modules/md-editor-v3/lib/style.css | 1 + node_modules/md-editor-v3/package.json | 124 + 56 files changed, 12363 insertions(+), 293 deletions(-) create mode 100644 backend/app/api/v1/upgrade.go delete mode 100644 backend/configs/cache.go delete mode 100644 backend/configs/csrf.go delete mode 100644 backend/middleware/csrf.go create mode 100644 frontend/src/views/setting/snapshot/status/index.vue create mode 100644 node_modules/.package-lock.json create mode 100644 node_modules/md-editor-v3/LICENSE create mode 100644 node_modules/md-editor-v3/README.md create mode 100644 node_modules/md-editor-v3/lib/MdEditor/Editor.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/config.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/extensions/DropdownToolbar.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/CatalogLink.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/index.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/extensions/ModalToolbar.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/extensions/NormalToolbar.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/index.d.ts create mode 100644 node_modules/md-editor-v3/lib/MdEditor/type.d.ts create mode 100644 node_modules/md-editor-v3/lib/md-editor-v3.es.js create mode 100644 node_modules/md-editor-v3/lib/md-editor-v3.umd.js create mode 100644 node_modules/md-editor-v3/lib/style.css create mode 100644 node_modules/md-editor-v3/package.json diff --git a/backend/app.yaml b/backend/app.yaml index 3ac56716c..20c856a77 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 #最大日志保留个数 - -cache: - path: /opt/1Panel/data/cache + log_backup: 10 -# 跨域配置 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 8dca968b2..043031ea9 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 000000000..0acb29271 --- /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 ea2a20205..47ad3da92 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 a436fb312..0aae20e48 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 93476e9e9..ce2e1c39e 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 ecf882092..a521e38c7 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 300ca2613..3c6852014 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 286e78ff2..931fd4e2d 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 90a839f2d..694fa6a5d 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 f818264b2..2a1954d73 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 1403014fb..ecd6bfbb5 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") + versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion")) + snap := model.Snapshot{ + Name: "1panel_snapshot_" + timeNow, + Description: req.Description, + From: req.From, + Version: versionItem.Value, + Status: constant.StatusWaiting, + } + _ = snapshotRepo.Create(&snap) + go func() { + 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 + } + _ = 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 +} - 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) +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 } - client, err := docker.NewDockerClient() + 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 } - ctx := context.Background() - info, err := client.Info(ctx) + client, err := NewIBackupService().NewClient(&backup) if err != nil { return err } - dataDir := info.DockerRootDir - stdout, err := cmd.Exec("systemctl stop docker") + localDir, err := loadLocalDir() if err != nil { - return errors.New(stdout) + 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) } - 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 + _ = 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 _, 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) + 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 := 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 := 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) } } - 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 + 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 _, 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 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) + } } } - 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 + 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 := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_backup_%s.tar.gz", timeNow), files.TarGz); err != nil { + if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { return err } - snap := model.Snapshot{ - Name: "1panel_backup_" + timeNow, - Description: req.Description, - BackupType: req.BackupType, - Status: constant.StatusWaiting, + return nil +} + +func updateSnapshotStatus(id uint, status string, message string) { + if status != constant.StatusSuccess { + global.LOG.Errorf("snapshot failed, err: %s", message) } - _ = 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 { - _ = 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 + 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) } - snap.Status = constant.StatusSuccess - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) - global.LOG.Infof("upload snapshot to %s success", backup.Type) - }() + 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 91549ba6b..0d8a61d24 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 { + backup, err := backupRepo.Get(commonRepo.WithByType("OSS")) + if err != nil { fmt.Println(err) } - backupAccont, err := NewIBackupService().NewClient(&backup) + client, err := NewIBackupService().NewClient(&backup) if err != nil { fmt.Println(err) } - fmt.Println(backupAccont.Upload("/Users/slooop/Downloads/1Panel.db", "database/1Panel.db")) + 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 6222464e2..000000000 --- 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 a38c7197a..e76d38297 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 11d3f9300..000000000 --- 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 076efa413..2a4b655d5 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 a843c8fa6..a55074f02 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 54d045193..7a68f663e 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 4bb9bca79..453e6b1d0 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 94c12ed16..412c575a5 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 28c32ab0c..000000000 --- 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 fe8347b8c..dcd5d91b7 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 b426b26e8..01df9a3aa 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 a62f534da..e135c85e4 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 4d6882b61..168203e6d 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 a4808b9d0..9c37f2845 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 fd7fadf85..639ac05cc 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 8da20fede..dbb7ec767 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 4c342648b..21ba5b17e 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 d5abdd110..ee4a07fb7 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 f9f17ff35..33969cc3c 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 33150fc52..bcb523a30 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 ceac85863..2a9fefb64 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 53d6b298c..905845092 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 1cb31ec98..799259d20 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 789e10572..c3267a07a 100644 --- a/frontend/src/views/setting/snapshot/index.vue +++ b/frontend/src/views/setting/snapshot/index.vue @@ -1,42 +1,70 @@