feat: 完成快照同步功能

pull/131/head
ssongliu 2023-02-13 15:48:18 +08:00 committed by ssongliu
parent 71349d2a63
commit 205d406761
27 changed files with 816 additions and 85 deletions

View File

@ -210,3 +210,30 @@ func (b *BaseApi) ListBackup(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Backup Account
// @Summary List files from backup accounts
// @Description 获取备份账号内文件列表
// @Accept json
// @Param request body dto.BackupSearchFile true "request"
// @Success 200 {anrry} string
// @Security ApiKeyAuth
// @Router /backups/search/files [post]
func (b *BaseApi) LoadFilesFromBackup(c *gin.Context) {
var req dto.BackupSearchFile
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
data, err := backupService.ListFiles(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}

View File

@ -40,13 +40,13 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
// @Summary Update mysql database description
// @Description 更新 mysql 数据库库描述信息
// @Accept json
// @Param request body dto.MysqlDescription true "request"
// @Param request body dto.UpdateDescription true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/description/update [post]
// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"database_mysqls","output_colume":"name","output_value":"name"}],"formatZH":"mysql 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mysql database [name] is modified => [description]"}
func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
var req dto.MysqlDescription
var req dto.UpdateDescription
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return

View File

@ -9,7 +9,7 @@ import (
)
// @Tags System Setting
// @Summary Create system backup
// @Summary Create system snapshot
// @Description 创建系统快照
// @Accept json
// @Param request body dto.SnapshotCreate true "request"
@ -34,6 +34,58 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags System Setting
// @Summary Import system snapshot
// @Description 导入已有快照
// @Accept json
// @Param request body dto.SnapshotImport true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/import [post]
// @x-panel-log {"bodyKeys":["from", "names"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"从 [from] 同步系统快照 [names]","formatEN":"Sync system snapshots [names] from [from]"}
func (b *BaseApi) ImportSnapshot(c *gin.Context) {
var req dto.SnapshotImport
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.SnapshotImport(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags System Setting
// @Summary Update snapshot description
// @Description 更新快照描述信息
// @Accept json
// @Param request body dto.UpdateDescription true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/description/update [post]
// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"快照 [name] 描述信息修改 [description]","formatEN":"The description of the snapshot [name] is modified => [description]"}
func (b *BaseApi) UpdateSnapDescription(c *gin.Context) {
var req dto.UpdateDescription
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.UpdateDescription(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags System Setting
// @Summary Page system snapshot
// @Description 获取系统快照列表分页

View File

@ -26,6 +26,10 @@ type BackupSearch struct {
DetailName string `json:"detailName"`
}
type BackupSearchFile struct {
Type string `json:"type" validate:"required"`
}
type RecordSearch struct {
PageInfo
Type string `json:"type" validate:"required"`

View File

@ -10,6 +10,11 @@ type PageInfo struct {
PageSize int `json:"pageSize" validate:"required,number"`
}
type UpdateDescription struct {
ID uint `json:"id" validate:"required"`
Description string `json:"description"`
}
type OperationWithName struct {
Name string `json:"name" validate:"required"`
}

View File

@ -2,11 +2,6 @@ package dto
import "time"
type MysqlDescription struct {
ID uint `json:"id" validate:"required"`
Description string `json:"description"`
}
type MysqlDBInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`

View File

@ -57,6 +57,11 @@ type SnapshotRecover struct {
ReDownload bool `json:"reDownload"`
ID uint `json:"id" validate:"required"`
}
type SnapshotImport struct {
From string `json:"from"`
Names []string `json:"names"`
Description string `json:"description"`
}
type SnapshotInfo struct {
ID uint `json:"id"`
Name string `json:"name"`

View File

@ -29,6 +29,8 @@ type IBackupService interface {
BatchDelete(ids []uint) error
BatchDeleteRecord(ids []uint) error
NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error)
ListFiles(req dto.BackupSearchFile) ([]interface{}, error)
}
func NewIBackupService() IBackupService {
@ -226,6 +228,18 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
return nil
}
func (u *BackupService) ListFiles(req dto.BackupSearchFile) ([]interface{}, error) {
backup, err := backupRepo.Get(backupRepo.WithByType(req.Type))
if err != nil {
return nil, err
}
client, err := u.NewClient(&backup)
if err != nil {
return nil, err
}
return client.ListObjects("system_snapshot/")
}
func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) {
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {

View File

@ -37,7 +37,7 @@ type IMysqlService interface {
ChangePassword(info dto.ChangeDBInfo) error
UpdateVariables(updatas []dto.MysqlVariablesUpdate) error
UpdateConfByFile(info dto.MysqlConfUpdateByFile) error
UpdateDescription(req dto.MysqlDescription) error
UpdateDescription(req dto.UpdateDescription) error
RecoverByUpload(req dto.UploadRecover) error
Backup(db dto.BackupDB) error
@ -201,7 +201,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
return &mysql, nil
}
func (u *MysqlService) UpdateDescription(req dto.MysqlDescription) error {
func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error {
return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
}

View File

@ -29,8 +29,10 @@ type ISnapshotService interface {
SnapshotCreate(req dto.SnapshotCreate) error
SnapshotRecover(req dto.SnapshotRecover) error
SnapshotRollback(req dto.SnapshotRecover) error
SnapshotImport(req dto.SnapshotImport) error
Delete(req dto.BatchDeleteReq) error
UpdateDescription(req dto.UpdateDescription) error
readFromJson(path string) (SnapshotJson, error)
}
@ -51,11 +53,48 @@ func (u *SnapshotService) SearchWithPage(req dto.SearchWithPage) (int64, interfa
return total, dtoSnap, err
}
func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
if len(req.Names) == 0 {
return fmt.Errorf("incorrect snapshot request body: %v", req.Names)
}
for _, snap := range req.Names {
nameItems := strings.Split(snap, "_")
if !strings.HasPrefix(snap, "1panel_v") || !strings.HasSuffix(snap, ".tar.gz") || len(nameItems) != 3 {
return fmt.Errorf("incorrect snapshot name format of %s", snap)
}
formatTime, err := time.Parse("20060102150405", strings.ReplaceAll(nameItems[2], ".tar.gz", ""))
if err != nil {
return fmt.Errorf("incorrect snapshot name format of %s", snap)
}
itemSnap := model.Snapshot{
Name: snap,
From: req.From,
Version: nameItems[1],
Description: req.Description,
Status: constant.StatusSuccess,
BaseModel: model.BaseModel{
CreatedAt: formatTime,
UpdatedAt: formatTime,
},
}
if err := snapshotRepo.Create(&itemSnap); err != nil {
return err
}
}
return nil
}
func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error {
return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
}
type SnapshotJson struct {
OldBaseDir string `json:"oldBaseDir"`
OldDockerDataDir string `json:"oldDockerDataDir"`
OldBackupDataDir string `json:"oldDackupDataDir"`
OldPanelDataDir string `json:"oldPanelDataDir"`
BaseDir string `json:"baseDir"`
DockerDataDir string `json:"dockerDataDir"`
BackupDataDir string `json:"backupDataDir"`
PanelDataDir string `json:"panelDataDir"`
@ -78,15 +117,15 @@ func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
}
timeNow := time.Now().Format("20060102150405")
rootDir := fmt.Sprintf("%s/system/1panel_snapshot_%s", localDir, timeNow)
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
rootDir := fmt.Sprintf("%s/system/1panel_%s_%s", localDir, versionItem.Value, timeNow)
backupPanelDir := fmt.Sprintf("%s/1panel", rootDir)
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
backupDockerDir := fmt.Sprintf("%s/docker", rootDir)
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
snap := model.Snapshot{
Name: "1panel_snapshot_" + timeNow,
Name: fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow),
Description: req.Description,
From: req.From,
Version: versionItem.Value,
@ -139,13 +178,19 @@ func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
return
}
snapJson := SnapshotJson{DockerDataDir: dockerDataDir, BackupDataDir: localDir, PanelDataDir: global.CONF.BaseDir + "/1panel", LiveRestoreEnabled: liveRestoreStatus}
snapJson := SnapshotJson{
BaseDir: global.CONF.BaseDir,
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 {
if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_%s_%s.tar.gz", versionItem.Value, timeNow), files.TarGz); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
@ -154,14 +199,14 @@ func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
global.LOG.Infof("start to upload snapshot to %s, please wait", backup.Type)
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusUploading})
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 {
localPath := fmt.Sprintf("%s/system/1panel_%s_%s.tar.gz", localDir, versionItem.Value, timeNow)
if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_%s_%s.tar.gz", versionItem.Value, timeNow)); err != nil || !ok {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err)
return
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
_ = os.RemoveAll(fmt.Sprintf("%s/system/1panel_snapshot_%s.tar.gz", localDir, timeNow))
_ = os.RemoveAll(fmt.Sprintf("%s/system/1panel_%s_%s.tar.gz", localDir, versionItem.Value, timeNow))
global.LOG.Infof("upload snapshot to %s success", backup.Type)
}()
@ -230,8 +275,6 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
isReTry = false
}
rootDir := fmt.Sprintf("%s/%s", baseDir, snap.Name)
u.OriginalPath = fmt.Sprintf("%s/original_%s", global.CONF.BaseDir, snap.Name)
_ = os.MkdirAll(u.OriginalPath, os.ModePerm)
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir))
if err != nil {
@ -241,7 +284,10 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
if snap.InterruptStep == "Readjson" {
isReTry = false
}
u.OriginalPath = fmt.Sprintf("%s/original_%s", snapJson.BaseDir, snap.Name)
_ = os.MkdirAll(u.OriginalPath, os.ModePerm)
snapJson.OldBaseDir = global.CONF.BaseDir
snapJson.OldPanelDataDir = global.CONF.BaseDir + "/1panel"
snapJson.OldBackupDataDir = localDir
recoverPanelDir := fmt.Sprintf("%s/%s/1panel", baseDir, snap.Name)
@ -340,10 +386,6 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
fileOp := files.NewFileOp()
rootDir := fmt.Sprintf("%s/system/%s/%s", localDir, snap.Name, snap.Name)
u.OriginalPath = fmt.Sprintf("%s/original_%s", global.CONF.BaseDir, snap.Name)
if _, err := os.Stat(u.OriginalPath); err != nil && os.IsNotExist(err) {
return fmt.Errorf("load original dir failed, err: %s", err)
}
_ = settingRepo.Update("SystemStatus", "Rollbacking")
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"rollback_status": constant.StatusWaiting})
@ -353,6 +395,10 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
updateRollbackStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
u.OriginalPath = fmt.Sprintf("%s/original_%s", snapJson.OldBaseDir, snap.Name)
if _, err := os.Stat(u.OriginalPath); err != nil && os.IsNotExist(err) {
return
}
_, _ = cmd.Exec("systemctl stop docker")
if err := u.handleDockerDatas(fileOp, "rollback", u.OriginalPath, snapJson.OldDockerDataDir); err != nil {

View File

@ -58,7 +58,7 @@ func (u *UpgradeService) SearchUpgrade() (*dto.UpgradeInfo, error) {
}
if len(releaseInfo.NewVersion) != 0 {
isNew, err := compareVersion(currentVersion.Value, releaseInfo.NewVersion)
if !isNew && err != nil {
if !isNew || err != nil {
return nil, err
}
return &releaseInfo, nil

View File

@ -17,6 +17,7 @@ func (s *BackupRouter) InitBackupRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
baRouter.GET("/search", baseApi.ListBackup)
baRouter.POST("/search/files", baseApi.LoadFilesFromBackup)
baRouter.POST("/buckets", baseApi.ListBuckets)
baRouter.POST("", baseApi.CreateBackup)
baRouter.POST("/del", baseApi.DeleteBackup)

View File

@ -27,9 +27,11 @@ 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/import", baseApi.ImportSnapshot)
settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot)
settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot)
settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot)
settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription)
settingRouter.POST("/upgrade", baseApi.Upgrade)
settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo)
settingRouter.GET("/basedir", baseApi.LoadBaseDir)

View File

@ -15,7 +15,7 @@ func init() {
}
var userinfoCmd = &cobra.Command{
Use: "userinfo",
Use: "user-info",
Short: "获取用户信息",
RunE: func(cmd *cobra.Command, args []string) error {
fullPath := "/opt/1panel/db/1Panel.db"

View File

@ -1169,6 +1169,42 @@ var doc = `{
}
}
},
"/backups/search/files": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取备份账号内文件列表",
"consumes": [
"application/json"
],
"tags": [
"Backup Account"
],
"summary": "List files from backup accounts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BackupSearchFile"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "anrry"
}
}
}
}
},
"/backups/update": {
"post": {
"security": [
@ -3907,7 +3943,7 @@ var doc = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.MysqlDescription"
"$ref": "#/definitions/dto.UpdateDescription"
}
}
],
@ -6285,7 +6321,7 @@ var doc = `{
"tags": [
"System Setting"
],
"summary": "Create system backup",
"summary": "Create system snapshot",
"parameters": [
{
"description": "request",
@ -6365,6 +6401,101 @@ var doc = `{
}
}
},
"/settings/snapshot/description/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新快照描述信息",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Update snapshot description",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateDescription"
}
}
],
"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",
"description"
],
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]",
"formatZH": "快照 [name] 描述信息修改 [description]",
"paramKeys": []
}
}
},
"/settings/snapshot/import": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "导入已有快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Import system snapshot",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotImport"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"from",
"names"
],
"formatEN": "Sync system snapshots [names] from [from]",
"formatZH": "从 [from] 同步系统快照 [names]",
"paramKeys": []
}
}
},
"/settings/snapshot/recover": {
"post": {
"security": [
@ -8474,6 +8605,17 @@ var doc = `{
}
}
},
"dto.BackupSearchFile": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string"
}
}
},
"dto.BatchDelete": {
"type": "object",
"required": [
@ -9751,20 +9893,6 @@ var doc = `{
}
}
},
"dto.MysqlDescription": {
"type": "object",
"required": [
"id"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
}
}
},
"dto.MysqlStatus": {
"type": "object",
"properties": {
@ -10451,6 +10579,23 @@ var doc = `{
}
}
},
"dto.SnapshotImport": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"from": {
"type": "string"
},
"names": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"dto.SnapshotRecover": {
"type": "object",
"required": [
@ -10468,6 +10613,20 @@ var doc = `{
}
}
},
"dto.UpdateDescription": {
"type": "object",
"required": [
"id"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
}
}
},
"dto.Upgrade": {
"type": "object",
"properties": {

View File

@ -1155,6 +1155,42 @@
}
}
},
"/backups/search/files": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取备份账号内文件列表",
"consumes": [
"application/json"
],
"tags": [
"Backup Account"
],
"summary": "List files from backup accounts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BackupSearchFile"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "anrry"
}
}
}
}
},
"/backups/update": {
"post": {
"security": [
@ -3893,7 +3929,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.MysqlDescription"
"$ref": "#/definitions/dto.UpdateDescription"
}
}
],
@ -6271,7 +6307,7 @@
"tags": [
"System Setting"
],
"summary": "Create system backup",
"summary": "Create system snapshot",
"parameters": [
{
"description": "request",
@ -6351,6 +6387,101 @@
}
}
},
"/settings/snapshot/description/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新快照描述信息",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Update snapshot description",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateDescription"
}
}
],
"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",
"description"
],
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]",
"formatZH": "快照 [name] 描述信息修改 [description]",
"paramKeys": []
}
}
},
"/settings/snapshot/import": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "导入已有快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Import system snapshot",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotImport"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"from",
"names"
],
"formatEN": "Sync system snapshots [names] from [from]",
"formatZH": "从 [from] 同步系统快照 [names]",
"paramKeys": []
}
}
},
"/settings/snapshot/recover": {
"post": {
"security": [
@ -8460,6 +8591,17 @@
}
}
},
"dto.BackupSearchFile": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string"
}
}
},
"dto.BatchDelete": {
"type": "object",
"required": [
@ -9737,20 +9879,6 @@
}
}
},
"dto.MysqlDescription": {
"type": "object",
"required": [
"id"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
}
}
},
"dto.MysqlStatus": {
"type": "object",
"properties": {
@ -10437,6 +10565,23 @@
}
}
},
"dto.SnapshotImport": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"from": {
"type": "string"
},
"names": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"dto.SnapshotRecover": {
"type": "object",
"required": [
@ -10454,6 +10599,20 @@
}
}
},
"dto.UpdateDescription": {
"type": "object",
"required": [
"id"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
}
}
},
"dto.Upgrade": {
"type": "object",
"properties": {

View File

@ -28,6 +28,13 @@ definitions:
- type
- vars
type: object
dto.BackupSearchFile:
properties:
type:
type: string
required:
- type
type: object
dto.BatchDelete:
properties:
names:
@ -890,15 +897,6 @@ definitions:
required:
- id
type: object
dto.MysqlDescription:
properties:
description:
type: string
id:
type: integer
required:
- id
type: object
dto.MysqlStatus:
properties:
Aborted_clients:
@ -1354,6 +1352,17 @@ definitions:
required:
- from
type: object
dto.SnapshotImport:
properties:
description:
type: string
from:
type: string
names:
items:
type: string
type: array
type: object
dto.SnapshotRecover:
properties:
id:
@ -1365,6 +1374,15 @@ definitions:
required:
- id
type: object
dto.UpdateDescription:
properties:
description:
type: string
id:
type: integer
required:
- id
type: object
dto.Upgrade:
properties:
version:
@ -3358,6 +3376,28 @@ paths:
summary: List buckets
tags:
- Backup Account
/backups/search/files:
post:
consumes:
- application/json
description: 获取备份账号内文件列表
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BackupSearchFile'
responses:
"200":
description: OK
schema:
type: anrry
security:
- ApiKeyAuth: []
summary: List files from backup accounts
tags:
- Backup Account
/backups/update:
post:
consumes:
@ -5100,7 +5140,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/dto.MysqlDescription'
$ref: '#/definitions/dto.UpdateDescription'
responses:
"200":
description: ""
@ -6621,7 +6661,7 @@ paths:
description: ""
security:
- ApiKeyAuth: []
summary: Create system backup
summary: Create system snapshot
tags:
- System Setting
x-panel-log:
@ -6665,6 +6705,68 @@ paths:
formatEN: Delete system backup [name]
formatZH: 删除系统快照 [name]
paramKeys: []
/settings/snapshot/description/update:
post:
consumes:
- application/json
description: 更新快照描述信息
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateDescription'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Update snapshot description
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
- description
formatEN: The description of the snapshot [name] is modified => [description]
formatZH: 快照 [name] 描述信息修改 [description]
paramKeys: []
/settings/snapshot/import:
post:
consumes:
- application/json
description: 导入已有快照
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SnapshotImport'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Import system snapshot
tags:
- System Setting
x-panel-log:
BeforeFuntions: []
bodyKeys:
- from
- names
formatEN: Sync system snapshots [names] from [from]
formatZH: 从 [from] 同步系统快照 [names]
paramKeys: []
/settings/snapshot/recover:
post:
consumes:

View File

@ -5,10 +5,6 @@ export namespace Database {
mysqlName: string;
dbName: string;
}
export interface DescriptionUpdate {
id: number;
description: string;
}
export interface Backup {
mysqlName: string;
dbName: string;

View File

@ -28,6 +28,10 @@ export interface CommonModel {
CreatedAt?: string;
UpdatedAt?: string;
}
export interface DescriptionUpdate {
id: number;
description: string;
}
// * 文件上传模块
export namespace Upload {

View File

@ -53,6 +53,11 @@ export namespace Setting {
from: string;
description: string;
}
export interface SnapshotImport {
from: string;
names: Array<string>;
description: string;
}
export interface SnapshotRecover {
id: number;
isNew: boolean;

View File

@ -6,6 +6,10 @@ export const getBackupList = () => {
return http.get<Array<Backup.BackupInfo>>(`/backups/search`);
};
export const getFilesFromBackup = (type: string) => {
return http.post<Array<any>>(`/backups/search/files`, { type: type });
};
export const addBackup = (params: Backup.BackupOperate) => {
return http.post<Backup.BackupOperate>(`/backups`, params);
};

View File

@ -1,5 +1,5 @@
import http from '@/api';
import { SearchWithPage, ReqPage, ResPage } from '../interface';
import { SearchWithPage, ReqPage, ResPage, DescriptionUpdate } from '../interface';
import { Database } from '../interface/database';
export const searchMysqlDBs = (params: SearchWithPage) => {
@ -25,7 +25,7 @@ export const updateMysqlAccess = (params: Database.ChangeInfo) => {
export const updateMysqlPassword = (params: Database.ChangeInfo) => {
return http.post(`/databases/change/password`, params);
};
export const updateMysqlDescription = (params: Database.DescriptionUpdate) => {
export const updateMysqlDescription = (params: DescriptionUpdate) => {
return http.post(`/databases/description/update`, params);
};
export const updateMysqlVariables = (params: Array<Database.VariablesUpdate>) => {

View File

@ -1,5 +1,5 @@
import http from '@/api';
import { ResPage, SearchWithPage } from '../interface';
import { ResPage, SearchWithPage, DescriptionUpdate } from '../interface';
import { Setting } from '../interface/setting';
export const getSettingInfo = () => {
@ -53,6 +53,12 @@ export const loadBaseDir = () => {
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
return http.post(`/settings/snapshot`, param);
};
export const snapshotImport = (param: Setting.SnapshotImport) => {
return http.post(`/settings/snapshot/import`, param);
};
export const updateSnapshotDescription = (param: DescriptionUpdate) => {
return http.post(`/settings/snapshot/description/update`, param);
};
export const snapshotDelete = (param: { ids: number[] }) => {
return http.post(`/settings/snapshot/del`, param);
};

View File

@ -770,6 +770,7 @@ export default {
thirdPartySupport: 'Only third-party accounts are supported',
recoverDetail: 'Recover detail',
createSnapshot: 'Create snapshot',
importSnapshot: 'Sync snapshot',
recover: 'Recover',
noRecoverRecord: 'No recovery record has been recorded',
lastRecoverAt: 'Last recovery time',

View File

@ -768,6 +768,7 @@ export default {
thirdPartySupport: '',
recoverDetail: '',
createSnapshot: '',
importSnapshot: '',
recover: '',
noRecoverRecord: '',
lastRecoverAt: '',

View File

@ -0,0 +1,124 @@
<template>
<div v-loading="loading">
<el-drawer v-model="drawerVisiable" size="50%">
<template #header>
<DrawerHeader :header="$t('setting.importSnapshot')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.backupAccount')" prop="from">
<el-select style="width: 100%" v-model="form.from" @change="loadFiles" clearable>
<el-option
v-for="item in backupOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="names">
<el-select style="width: 100%" v-model="form.names" multiple clearable>
<el-option v-for="item in fileNames" :key="item" :value="item" :label="item" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" clearable v-model="form.description" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="submitImport(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { ElMessage, FormInstance } from 'element-plus';
import i18n from '@/lang';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { snapshotImport } from '@/api/modules/setting';
import { getBackupList, getFilesFromBackup } from '@/api/modules/backup';
import { loadBackupName } from '../../helper';
import { Rules } from '@/global/form-rules';
const drawerVisiable = ref(false);
const loading = ref();
const formRef = ref();
const backupOptions = ref();
const fileNames = ref();
const form = reactive({
from: '',
names: [],
description: '',
});
const rules = reactive({
from: [Rules.requiredSelect],
name: [Rules.requiredSelect],
});
const acceptParams = (): void => {
form.from = '';
form.names = [] as Array<string>;
loadBackups();
drawerVisiable.value = true;
};
const emit = defineEmits(['search']);
const handleClose = () => {
drawerVisiable.value = false;
};
const submitImport = async (formEl: FormInstance | undefined) => {
loading.value = true;
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
await snapshotImport(form)
.then(() => {
emit('search');
loading.value = false;
drawerVisiable.value = false;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const loadBackups = async () => {
const res = await getBackupList();
backupOptions.value = [];
for (const item of res.data) {
if (item.type !== 'LOCAL' && item.id !== 0) {
backupOptions.value.push({ label: loadBackupName(item.type), value: item.type });
}
}
};
const loadFiles = async () => {
const res = await getFilesFromBackup(form.from);
fileNames.value = res.data || [];
for (let i = 0; i < fileNames.value.length; i++) {
fileNames.value[i] = fileNames.value[i].replaceAll('system_snapshot/', '');
}
};
defineExpose({
acceptParams,
});
</script>

View File

@ -7,6 +7,9 @@
<el-button type="primary" @click="onCreate()">
{{ $t('setting.createSnapshot') }}
</el-button>
<el-button @click="onImport()">
{{ $t('setting.importSnapshot') }}
</el-button>
<el-button type="primary" plain :disabled="selects.length === 0" @click="batchDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
@ -43,14 +46,9 @@
prop="name"
fix
/>
<el-table-column
:label="$t('commons.table.description')"
min-width="150"
show-overflow-tooltip
prop="description"
/>
<el-table-column :label="$t('setting.backupAccount')" min-width="150" prop="from" />
<el-table-column :label="$t('setting.backup')" min-width="80" prop="status">
<el-table-column prop="version" :label="$t('app.version')" />
<el-table-column :label="$t('setting.backupAccount')" min-width="80" prop="from" />
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === 'Success'" type="success">
{{ $t('commons.table.statusSuccess') }}
@ -72,6 +70,13 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.description')" prop="description">
<template #default="{ row }">
<fu-read-write-switch :data="row.description" v-model="row.edit" @change="onChange(row)">
<el-input v-model="row.description" @blur="row.edit = false" />
</fu-read-write-switch>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
@ -89,6 +94,7 @@
</template>
</LayoutContent>
<RecoverStatus ref="recoverStatusRef" @search="search()"></RecoverStatus>
<SnapshotImport ref="importRef" @search="search()" />
<el-drawer v-model="drawerVisiable" size="50%">
<template #header>
<DrawerHeader :header="$t('setting.createSnapshot')" :back="handleClose" />
@ -139,7 +145,7 @@
<script setup lang="ts">
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import { snapshotCreate, searchSnapshotPage, snapshotDelete } from '@/api/modules/setting';
import { snapshotCreate, searchSnapshotPage, snapshotDelete, updateSnapshotDescription } from '@/api/modules/setting';
import { onMounted, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/util';
import { useDeleteData } from '@/hooks/use-delete-data';
@ -150,6 +156,7 @@ import { ElMessage } from 'element-plus';
import { Setting } from '@/api/interface/setting';
import LayoutContent from '@/layout/layout-content.vue';
import RecoverStatus from '@/views/setting/snapshot/status/index.vue';
import SnapshotImport from '@/views/setting/snapshot/import/index.vue';
import { getBackupList } from '@/api/modules/backup';
import { loadBackupName } from '../helper';
@ -164,6 +171,7 @@ const paginationConfig = reactive({
const searchName = ref();
const recoverStatusRef = ref();
const importRef = ref();
const isRecordShow = ref();
const backupOptions = ref();
type FormInstance = InstanceType<typeof ElForm>;
@ -184,10 +192,21 @@ const onCreate = async () => {
drawerVisiable.value = true;
};
const onImport = () => {
importRef.value.acceptParams();
};
const handleClose = () => {
drawerVisiable.value = false;
};
const onChange = async (info: any) => {
if (!info.edit) {
await updateSnapshotDescription({ id: info.id, description: info.description });
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}
};
const submitAddSnapshot = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {