From aa2bb73199e58d5a24a90039c3337513d07a4e45 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Tue, 21 Feb 2023 19:06:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=89=8D=E7=AB=AF=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/backup.go | 125 +++++++++ backend/app/api/v1/database_mysql.go | 84 ------- backend/app/api/v1/database_redis.go | 40 --- backend/app/api/v1/file.go | 6 +- backend/app/api/v1/website.go | 77 ------ backend/app/dto/backup.go | 12 + backend/app/repo/app.go | 1 + backend/app/service/app_install.go | 10 - backend/app/service/app_utils.go | 119 +-------- backend/app/service/backup.go | 18 +- backend/app/service/backup_app.go | 199 +++++++++++++++ backend/app/service/backup_database.go | 170 +++++++++++++ backend/app/service/backup_redis.go | 132 ++++++++++ backend/app/service/backup_website.go | 238 ++++++++++++++++++ backend/app/service/cronjob_helper.go | 84 ++++--- backend/app/service/database_mysql.go | 170 +------------ backend/app/service/database_redis.go | 88 ------- backend/app/service/docker.go | 4 +- backend/app/service/image_repo.go | 4 +- backend/app/service/website.go | 81 +----- backend/app/service/website_utils.go | 181 ------------- backend/router/ro_database.go | 5 - backend/router/ro_setting.go | 3 + backend/router/ro_website.go | 3 - backend/utils/files/file_op.go | 16 ++ frontend/components.d.ts | 1 + frontend/src/api/interface/backup.ts | 11 + frontend/src/api/interface/database.ts | 4 - frontend/src/api/modules/database.ts | 13 - frontend/src/api/modules/setting.ts | 9 + frontend/src/api/modules/website.ts | 12 - .../mysql => components}/backup/index.vue | 59 +++-- .../app-store/installed/backup/index.vue | 202 --------------- .../src/views/app-store/installed/index.vue | 11 +- frontend/src/views/database/mysql/index.vue | 9 +- .../src/views/database/mysql/upload/index.vue | 13 +- .../redis/setting/persistence/index.vue | 5 +- .../views/website/website/backup/index.vue | 186 -------------- frontend/src/views/website/website/index.vue | 8 +- .../views/website/website/upload/index.vue | 13 +- 40 files changed, 1064 insertions(+), 1362 deletions(-) create mode 100644 backend/app/service/backup_app.go create mode 100644 backend/app/service/backup_database.go create mode 100644 backend/app/service/backup_redis.go create mode 100644 backend/app/service/backup_website.go rename frontend/src/{views/database/mysql => components}/backup/index.vue (77%) delete mode 100644 frontend/src/views/app-store/installed/backup/index.vue delete mode 100644 frontend/src/views/website/website/backup/index.vue diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go index 9dde87c47..5245ba37f 100644 --- a/backend/app/api/v1/backup.go +++ b/backend/app/api/v1/backup.go @@ -237,3 +237,128 @@ func (b *BaseApi) LoadFilesFromBackup(c *gin.Context) { helper.SuccessWithData(c, data) } + +// @Tags Backup Account +// @Summary Backup system data +// @Description 备份系统数据 +// @Accept json +// @Param request body dto.CommonBackup true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/ [post] +// @x-panel-log {"bodyKeys":["type","name","detailName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"备份 [type] 数据 [name][detailName]","formatEN":"backup [type] data [name][detailName]"} +func (b *BaseApi) Backup(c *gin.Context) { + var req dto.CommonBackup + 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 + } + + switch req.Type { + case "app": + if err := backupService.AppBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "mysql": + if err := backupService.MysqlBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "redis": + if err := backupService.RedisBackup(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Recover system data +// @Description 恢复系统数据 +// @Accept json +// @Param request body dto.CommonRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/recover [post] +// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} +func (b *BaseApi) Recover(c *gin.Context) { + var req dto.CommonRecover + 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 + } + + switch req.Type { + case "mysql": + if err := backupService.MysqlRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Recover system data by upload +// @Description 从上传恢复系统数据 +// @Accept json +// @Param request body dto.CommonRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/recover/byupload [post] +// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} +func (b *BaseApi) RecoverByUpload(c *gin.Context) { + var req dto.CommonRecover + 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 + } + + switch req.Type { + case "app": + if err := backupService.AppRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "mysql": + if err := backupService.MysqlRecoverByUpload(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "redis": + if err := backupService.RedisRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} diff --git a/backend/app/api/v1/database_mysql.go b/backend/app/api/v1/database_mysql.go index 0e050110e..ae6532033 100644 --- a/backend/app/api/v1/database_mysql.go +++ b/backend/app/api/v1/database_mysql.go @@ -210,90 +210,6 @@ func (b *BaseApi) ListDBName(c *gin.Context) { helper.SuccessWithData(c, list) } -// @Tags Database Mysql -// @Summary Backup mysql database -// @Description 备份 mysql 数据库 -// @Accept json -// @Param request body dto.BackupDB true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/backup [post] -// @x-panel-log {"bodyKeys":["mysqlName","dbName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"备份 mysql 数据库 [mysqlName][dbName]","formatEN":"backup mysql database [mysqlName][dbName]"} -func (b *BaseApi) BackupMysql(c *gin.Context) { - var req dto.BackupDB - 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 := mysqlService.Backup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Recover mysql database by upload file -// @Description Mysql 数据库从上传文件恢复 -// @Accept json -// @Param request body dto.UploadRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/recover/byupload [post] -// @x-panel-log {"bodyKeys":["fileDir","fileName","mysqlName","dbName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"mysql 数据库从 [fileDir]/[fileName] 恢复 [mysqlName][dbName]","formatEN":"mysql database recover [fileDir]/[fileName] from [mysqlName][dbName]"} -func (b *BaseApi) RecoverMysqlByUpload(c *gin.Context) { - var req dto.UploadRecover - 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 := mysqlService.RecoverByUpload(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Recover mysql database -// @Description Mysql 数据库恢复 -// @Accept json -// @Param request body dto.RecoverDB true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/recover [post] -// @x-panel-log {"bodyKeys":["mysqlName","dbName","backupName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"恢复 mysql 数据库 [mysqlName][dbName] [backupName]","formatEN":"恢复 mysql 数据库 [mysqlName][dbName] [backupName]"} -func (b *BaseApi) RecoverMysql(c *gin.Context) { - var req dto.RecoverDB - 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 := mysqlService.Recover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - // @Tags Database Mysql // @Summary Check before delete mysql database // @Description Mysql 数据库删除前检查 diff --git a/backend/app/api/v1/database_redis.go b/backend/app/api/v1/database_redis.go index 7cb5517dc..f3629cd07 100644 --- a/backend/app/api/v1/database_redis.go +++ b/backend/app/api/v1/database_redis.go @@ -139,46 +139,6 @@ func (b *BaseApi) UpdateRedisPersistenceConf(c *gin.Context) { helper.SuccessWithData(c, nil) } -// @Tags Database Redis -// @Summary Backup redis -// @Description 备份 redis 数据库 -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/backup [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFuntions":[],"formatZH":"备份 redis 数据库","formatEN":"backup redis database"} -func (b *BaseApi) RedisBackup(c *gin.Context) { - if err := redisService.Backup(); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Redis -// @Summary Recover redis -// @Description 恢复 redis 数据库 -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/recover [post] -// @x-panel-log {"bodyKeys":["fileDir","fileName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"redis 数据库从 [fileDir]/[fileName] 恢复","formatEN":"redis database recover from [fileDir]/[fileName]"} -func (b *BaseApi) RedisRecover(c *gin.Context) { - var req dto.RedisBackupRecover - 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 := redisService.Recover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - // @Tags Database Redis // @Summary Page redis backups // @Description 获取 redis 备份记录分页 diff --git a/backend/app/api/v1/file.go b/backend/app/api/v1/file.go index 0f0b4effa..866324223 100644 --- a/backend/app/api/v1/file.go +++ b/backend/app/api/v1/file.go @@ -272,10 +272,8 @@ func (b *BaseApi) UploadFiles(c *gin.Context) { dir := path.Dir(paths[0]) if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(dir, os.ModePerm); err != nil { - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("mkdir %s failed, err: %v", dir, err)) - return - } + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("mkdir %s failed, err: %v", dir, err)) + return } } success := 0 diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 90e6c8d80..49ce1e548 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -5,7 +5,6 @@ import ( "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" "github.com/gin-gonic/gin" ) @@ -113,82 +112,6 @@ func (b *BaseApi) OpWebsite(c *gin.Context) { helper.SuccessWithData(c, nil) } -// @Tags Website -// @Summary Backup website -// @Description 备份网站 -// @Accept json -// @Param request body request.WebsiteResourceReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/backup [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"websites","output_colume":"primary_domain","output_value":"domain"}],"formatZH":"备份网站 [domain]","formatEN":"Backup website [domain]"} -func (b *BaseApi) BackupWebsite(c *gin.Context) { - var req request.WebsiteResourceReq - if err := c.ShouldBindJSON(&req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - if err := websiteService.Backup(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Recover website by upload -// @Description 从上传恢复网站 -// @Accept json -// @Param request body request.WebsiteRecoverByFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/recover/byupload [post] -// @x-panel-log {"bodyKeys":["websiteName","fileDir","fileName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"[websiteName] 从上传恢复 [fileDir]/[fileName]","formatEN":"[websiteName] recover from uploads [fileDir]/[fileName]"} -func (b *BaseApi) RecoverWebsiteByUpload(c *gin.Context) { - var req request.WebsiteRecoverByFile - 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 := websiteService.RecoverByUpload(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Recover website -// @Description 从备份恢复网站 -// @Accept json -// @Param request body request.WebsiteRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/recover [post] -// @x-panel-log {"bodyKeys":["websiteName","backupName"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"[websiteName] 从备份恢复 [backupName]","formatEN":"[websiteName] recover from backups [backupName]"} -func (b *BaseApi) RecoverWebsite(c *gin.Context) { - var req request.WebsiteRecover - 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 := websiteService.Recover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - // @Tags Website // @Summary Delete website // @Description 删除网站 diff --git a/backend/app/dto/backup.go b/backend/app/dto/backup.go index 40d5ac09e..9e1685ddb 100644 --- a/backend/app/dto/backup.go +++ b/backend/app/dto/backup.go @@ -30,6 +30,18 @@ type BackupSearchFile struct { Type string `json:"type" validate:"required"` } +type CommonBackup struct { + Type string `json:"type" validate:"required,oneof=app mysql redis website"` + Name string `json:"name"` + DetailName string `json:"detailName"` +} +type CommonRecover struct { + Type string `json:"type" validate:"required,oneof=app mysql redis website"` + Name string `json:"name"` + DetailName string `json:"detailName"` + File string `json:"file"` +} + type RecordSearch struct { PageInfo Type string `json:"type" validate:"required"` diff --git a/backend/app/repo/app.go b/backend/app/repo/app.go index 020b343f7..01e6cde2b 100644 --- a/backend/app/repo/app.go +++ b/backend/app/repo/app.go @@ -2,6 +2,7 @@ package repo import ( "context" + "github.com/1Panel-dev/1Panel/backend/app/model" "gorm.io/gorm" "gorm.io/gorm/clause" diff --git a/backend/app/service/app_install.go b/backend/app/service/app_install.go index eab4dc456..b39706fff 100644 --- a/backend/app/service/app_install.go +++ b/backend/app/service/app_install.go @@ -187,16 +187,6 @@ func (a AppInstallService) Operate(req request.AppInstalledOperate) error { return nil case constant.Sync: return syncById(install.ID) - case constant.Backup: - tx, ctx := getTxAndContext() - if err := backupInstall(ctx, install); err != nil { - tx.Rollback() - return err - } - tx.Commit() - return nil - case constant.Restore: - return restoreInstall(install, req.BackupId) case constant.Update: return updateInstall(install.ID, req.DetailId) default: diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index e40e4ac21..61d81a61d 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -3,14 +3,12 @@ package service import ( "context" "encoding/json" - "fmt" "math" "os" "path" "reflect" "strconv" "strings" - "time" "github.com/1Panel-dev/1Panel/backend/app/dto/response" "github.com/1Panel-dev/1Panel/backend/buserr" @@ -24,7 +22,6 @@ import ( "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/joho/godotenv" "github.com/pkg/errors" - "gopkg.in/yaml.v3" ) type DatabaseOp string @@ -177,11 +174,9 @@ func updateInstall(installId uint, detailId uint) error { if install.Version == detail.Version { return errors.New("two version is same") } - tx, ctx := getTxAndContext() - if err := backupInstall(ctx, install); err != nil { + if err := NewIBackupService().AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name}); err != nil { return err } - tx.Commit() if _, err = compose.Down(install.GetComposePath()); err != nil { return err } @@ -198,118 +193,6 @@ func updateInstall(installId uint, detailId uint) error { return appInstallRepo.Save(&install) } -func backupInstall(ctx context.Context, install model.AppInstall) error { - var backup model.AppInstallBackup - appPath := install.GetPath() - - backupAccount, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) - if err != nil { - return err - } - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backupAccount.Vars), &varMap); err != nil { - return err - } - dir, ok := varMap["dir"] - if !ok { - return errors.New("load local backup dir failed") - } - baseDir, ok := dir.(string) - if !ok { - return errors.New("load local backup dir failed") - } - backupDir := path.Join(baseDir, "apps", install.App.Key, install.Name) - fileOp := files.NewFileOp() - if !fileOp.Stat(backupDir) { - _ = fileOp.CreateDir(backupDir, 0775) - } - now := time.Now() - day := now.Format("20060102150405") - fileName := fmt.Sprintf("%s_%s%s", install.Name, day, ".tar.gz") - if err := fileOp.Compress([]string{appPath}, backupDir, fileName, files.TarGz); err != nil { - return err - } - backup.Name = fileName - backup.Path = backupDir - backup.AppInstallId = install.ID - backup.AppDetailId = install.AppDetailId - backup.Param = install.Param - - return appInstallBackupRepo.Create(ctx, backup) -} - -func restoreInstall(install model.AppInstall, backupId uint) error { - backup, err := appInstallBackupRepo.GetFirst(commonRepo.WithByID(backupId)) - if err != nil { - return err - } - if _, err := compose.Down(install.GetComposePath()); err != nil { - return err - } - installKeyDir := path.Join(constant.AppInstallDir, install.App.Key) - installDir := path.Join(installKeyDir, install.Name) - backupFile := path.Join(backup.Path, backup.Name) - fileOp := files.NewFileOp() - if !fileOp.Stat(backupFile) { - return errors.New(fmt.Sprintf("%s file is not exist", backup.Name)) - } - - backupDir, err := fileOp.Backup(installDir) - if err != nil { - return err - } - if err := fileOp.Decompress(backupFile, installKeyDir, files.TarGz); err != nil { - return err - } - composeContent, err := os.ReadFile(install.GetComposePath()) - if err != nil { - return err - } - install.DockerCompose = string(composeContent) - envContent, err := os.ReadFile(path.Join(installDir, ".env")) - if err != nil { - return err - } - install.Env = string(envContent) - envMaps, err := godotenv.Unmarshal(string(envContent)) - if err != nil { - return err - } - install.HttpPort = 0 - httpPort, ok := envMaps["PANEL_APP_PORT_HTTP"] - if ok { - httpPortN, _ := strconv.Atoi(httpPort) - install.HttpPort = httpPortN - } - install.HttpsPort = 0 - httpsPort, ok := envMaps["PANEL_APP_PORT_HTTPS"] - if ok { - httpsPortN, _ := strconv.Atoi(httpsPort) - install.HttpsPort = httpsPortN - } - - composeMap := make(map[string]interface{}) - if err := yaml.Unmarshal(composeContent, &composeMap); err != nil { - return err - } - servicesMap := composeMap["services"].(map[string]interface{}) - for k, v := range servicesMap { - install.ServiceName = k - value := v.(map[string]interface{}) - install.ContainerName = value["container_name"].(string) - } - - install.Param = backup.Param - _ = fileOp.DeleteDir(backupDir) - if out, err := compose.Up(install.GetComposePath()); err != nil { - return handleErr(install, err, out) - } - install.AppDetailId = backup.AppDetailId - install.Version = backup.AppDetail.Version - install.Status = constant.Running - return appInstallRepo.Save(&install) -} - func getContainerNames(install model.AppInstall) ([]string, error) { composeMap := install.DockerCompose envMap := make(map[string]string) diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go index 9375a2cff..bbeaa48c1 100644 --- a/backend/app/service/backup.go +++ b/backend/app/service/backup.go @@ -31,6 +31,20 @@ type IBackupService interface { NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) ListFiles(req dto.BackupSearchFile) ([]interface{}, error) + + MysqlBackup(db dto.CommonBackup) error + MysqlRecover(db dto.CommonRecover) error + MysqlRecoverByUpload(req dto.CommonRecover) error + + RedisBackup() error + RedisRecover(db dto.CommonRecover) error + + WebsiteBackup(db dto.CommonBackup) error + WebsiteRecover(req dto.CommonRecover) error + WebsiteRecoverByUpload(req dto.CommonRecover) error + + AppBackup(db dto.CommonBackup) error + AppRecover(req dto.CommonRecover) error } func NewIBackupService() IBackupService { @@ -285,9 +299,7 @@ func loadLocalDir() (string, error) { if ok { if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(baseDir, os.ModePerm); err != nil { - if err != nil { - return "", fmt.Errorf("mkdir %s failed, err: %v", baseDir, err) - } + return "", fmt.Errorf("mkdir %s failed, err: %v", baseDir, err) } } return baseDir, nil diff --git a/backend/app/service/backup_app.go b/backend/app/service/backup_app.go new file mode 100644 index 000000000..60f9dc069 --- /dev/null +++ b/backend/app/service/backup_app.go @@ -0,0 +1,199 @@ +package service + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/compose" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/joho/godotenv" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +func (u *BackupService) AppBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) + if err != nil { + return err + } + install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) + if err != nil { + return err + } + timeNow := time.Now().Format("20060102150405") + backupDir := fmt.Sprintf("%s/app/%s/%s", localDir, req.Name, req.DetailName) + + fileName := fmt.Sprintf("%s_%s.tar.gz", req.DetailName, timeNow) + if err := handleAppBackup(&install, backupDir, fileName); err != nil { + return err + } + + record := &model.BackupRecord{ + Type: "app", + Name: req.Name, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: backupDir, + FileName: fileName, + } + + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + return nil +} + +func (u *BackupService) AppRecover(req dto.CommonRecover) error { + app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) + if err != nil { + return err + } + install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) + if err != nil { + return err + } + + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return errors.New(fmt.Sprintf("%s file is not exist", req.File)) + } + if _, err := compose.Down(install.GetComposePath()); err != nil { + return err + } + if err := handleAppRecover(&install, req.File); err != nil { + return err + } + return nil +} + +type AppInfo struct { + AppDetailId uint `json:"appDetailId"` + Param string `json:"param"` + Version string `json:"version"` +} + +func handleAppBackup(install *model.AppInstall, backupDir, fileName string) error { + fileOp := files.NewFileOp() + tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) + if !fileOp.Stat(tmpDir) { + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + var appInfo AppInfo + appInfo.Param = install.Param + appInfo.AppDetailId = install.AppDetailId + appInfo.Version = install.Version + remarkInfo, _ := json.Marshal(appInfo) + remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir) + if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil { + return err + } + + appPath := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, install.App.Key, install.Name) + if err := fileOp.Compress([]string{appPath}, tmpDir, "app.tar.gz", files.TarGz); err != nil { + return err + } + if err := fileOp.Compress([]string{tmpDir}, backupDir, fileName, files.TarGz); err != nil { + return err + } + return nil +} + +func handleAppRecover(install *model.AppInstall, recoverFile string) error { + fileOp := files.NewFileOp() + if err := fileOp.Decompress(recoverFile, path.Dir(recoverFile), files.TarGz); err != nil { + return err + } + tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") + defer func() { + _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) + }() + + if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { + return errors.New("the wrong recovery package does not have app.json or app.tar.gz files") + } + appjson, err := os.ReadFile(tmpPath + "/" + "app.json") + if err != nil { + return err + } + var appInfo AppInfo + _ = json.Unmarshal(appjson, &appInfo) + + if err := fileOp.Decompress(tmpPath+"/app.tar.gz", fmt.Sprintf("%s/%s", constant.AppInstallDir, install.App.Key), files.TarGz); err != nil { + return err + } + composeContent, err := os.ReadFile(install.GetComposePath()) + if err != nil { + return err + } + install.DockerCompose = string(composeContent) + envContent, err := os.ReadFile(fmt.Sprintf("%s/%s/%s/.env", constant.AppInstallDir, install.App.Key, install.Name)) + if err != nil { + return err + } + install.Env = string(envContent) + envMaps, err := godotenv.Unmarshal(string(envContent)) + if err != nil { + return err + } + install.HttpPort = 0 + httpPort, ok := envMaps["PANEL_APP_PORT_HTTP"] + if ok { + httpPortN, _ := strconv.Atoi(httpPort) + install.HttpPort = httpPortN + } + install.HttpsPort = 0 + httpsPort, ok := envMaps["PANEL_APP_PORT_HTTPS"] + if ok { + httpsPortN, _ := strconv.Atoi(httpsPort) + install.HttpsPort = httpsPortN + } + + composeMap := make(map[string]interface{}) + if err := yaml.Unmarshal(composeContent, &composeMap); err != nil { + return err + } + servicesMap := composeMap["services"].(map[string]interface{}) + for k, v := range servicesMap { + install.ServiceName = k + value := v.(map[string]interface{}) + install.ContainerName = value["container_name"].(string) + } + + install.Param = appInfo.Param + if out, err := compose.Up(install.GetComposePath()); err != nil { + install.Message = err.Error() + if len(out) != 0 { + install.Message = out + } + return errors.New(out) + } + install.AppDetailId = appInfo.AppDetailId + install.Version = appInfo.Version + install.Status = constant.Running + if err := appInstallRepo.Save(install); err != nil { + return err + } + return nil +} diff --git a/backend/app/service/backup_database.go b/backend/app/service/backup_database.go new file mode 100644 index 000000000..543b2f270 --- /dev/null +++ b/backend/app/service/backup_database.go @@ -0,0 +1,170 @@ +package service + +import ( + "compress/gzip" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/app/repo" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) MysqlBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + app, err := appInstallRepo.LoadBaseInfo("mysql", "") + if err != nil { + return err + } + + timeNow := time.Now().Format("20060102150405") + backupDir := fmt.Sprintf("%s/database/mysql/%s/%s", localDir, req.Name, req.DetailName) + fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow) + if err := handleMysqlBackup(app, backupDir, req.DetailName, fileName); err != nil { + return err + } + record := &model.BackupRecord{ + Type: "mysql", + Name: app.Name, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: backupDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + return nil +} + +func (u *BackupService) MysqlRecover(req dto.CommonRecover) error { + app, err := appInstallRepo.LoadBaseInfo("mysql", "") + if err != nil { + return err + } + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return errors.New(fmt.Sprintf("%s file is not exist", req.File)) + } + global.LOG.Infof("recover database %s-%s from backup file %s", req.Name, req.DetailName, req.File) + if err := handleMysqlRecover(app, path.Dir(req.File), req.DetailName, path.Base(req.File)); err != nil { + return err + } + return nil +} + +func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error { + app, err := appInstallRepo.LoadBaseInfo("mysql", "") + if err != nil { + return err + } + file := req.File + fileName := path.Base(req.File) + if !strings.HasSuffix(fileName, ".sql") && !strings.HasSuffix(fileName, ".gz") { + fileOp := files.NewFileOp() + fileNameItem := time.Now().Format("20060102150405") + dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) + if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) + } + } + var compressType files.CompressType + switch { + case strings.HasSuffix(fileName, ".tar.gz"), strings.HasSuffix(fileName, ".tgz"): + compressType = files.TarGz + case strings.HasSuffix(fileName, ".zip"): + compressType = files.Zip + } + if err := fileOp.Decompress(req.File, dstDir, compressType); err != nil { + _ = os.RemoveAll(dstDir) + return err + } + global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) + hasTestSql := false + _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if !info.IsDir() && info.Name() == "test.sql" { + hasTestSql = true + file = path + fileName = "test.sql" + } + return nil + }) + if !hasTestSql { + _ = os.RemoveAll(dstDir) + return fmt.Errorf("no such file named test.sql in %s, err: %v", fileName, err) + } + defer func() { + _ = os.RemoveAll(dstDir) + }() + } + + if err := handleMysqlRecover(app, path.Dir(file), req.DetailName, fileName); err != nil { + return err + } + global.LOG.Info("recover from uploads successful!") + return nil +} + +func handleMysqlBackup(app *repo.RootInfo, backupDir, dbName, fileName string) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(backupDir) { + if err := os.MkdirAll(backupDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + outfile, _ := os.OpenFile(backupDir+"/"+fileName, os.O_RDWR|os.O_CREATE, 0755) + global.LOG.Infof("start to mysqldump | gzip > %s.gzip", backupDir+"/"+fileName) + cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, dbName) + gzipCmd := exec.Command("gzip", "-cf") + gzipCmd.Stdin, _ = cmd.StdoutPipe() + gzipCmd.Stdout = outfile + _ = gzipCmd.Start() + _ = cmd.Run() + _ = gzipCmd.Wait() + + return nil +} + +func handleMysqlRecover(mysqlInfo *repo.RootInfo, recoverDir, dbName, fileName string) error { + file := recoverDir + "/" + fileName + fi, _ := os.Open(file) + defer fi.Close() + cmd := exec.Command("docker", "exec", "-i", mysqlInfo.ContainerName, "mysql", "-uroot", "-p"+mysqlInfo.Password, dbName) + if strings.HasSuffix(fileName, ".gz") { + gzipFile, err := os.Open(file) + if err != nil { + return err + } + defer gzipFile.Close() + gzipReader, err := gzip.NewReader(gzipFile) + if err != nil { + return err + } + defer gzipReader.Close() + cmd.Stdin = gzipReader + } else { + cmd.Stdin = fi + } + stdout, err := cmd.CombinedOutput() + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { + return errors.New(stdStr) + } + return nil +} diff --git a/backend/app/service/backup_redis.go b/backend/app/service/backup_redis.go new file mode 100644 index 000000000..cfdf93cb2 --- /dev/null +++ b/backend/app/service/backup_redis.go @@ -0,0 +1,132 @@ +package service + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/app/repo" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" + "github.com/1Panel-dev/1Panel/backend/utils/compose" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) RedisBackup() error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", "") + if err != nil { + return err + } + appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") + if err != nil { + return err + } + global.LOG.Infof("appendonly in redis conf is %s", appendonly) + + timeNow := time.Now().Format("20060102150405") + fileName := fmt.Sprintf("%s.rdb", timeNow) + if appendonly == "yes" { + fileName = fmt.Sprintf("%s.tar.gz", timeNow) + } + backupDir := fmt.Sprintf("%s/database/redis/%s/", localDir, redisInfo.Name) + if err := handleBackupRedis(redisInfo, backupDir, fileName); err != nil { + return err + } + record := &model.BackupRecord{ + Type: "redis", + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: backupDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + + return nil +} + +func (u *BackupService) RedisRecover(req dto.CommonRecover) error { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", "") + if err != nil { + return err + } + global.LOG.Infof("recover redis from backup file %s", req.File) + if err := handleRecoverRedis(redisInfo, req.File); err != nil { + return err + } + return nil +} + +func handleBackupRedis(redisInfo *repo.RootInfo, backupDir, fileName string) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(backupDir) { + if err := os.MkdirAll(backupDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + + stdout, err := cmd.Execf("docker exec %s redis-cli -a %s --no-auth-warning save", redisInfo.ContainerName, redisInfo.Password) + if err != nil { + return errors.New(string(stdout)) + } + + if strings.HasSuffix(fileName, ".tar.gz") { + redisDataDir := fmt.Sprintf("%s/%s/%s/data/appendonlydir", constant.AppInstallDir, "redis", redisInfo.Name) + if err := handleTar(redisDataDir, backupDir, fileName, ""); err != nil { + return err + } + return nil + } + + stdout1, err1 := cmd.Execf("docker cp %s:/data/dump.rdb %s/%s", redisInfo.ContainerName, backupDir, fileName) + if err1 != nil { + return errors.New(string(stdout1)) + } + return nil +} + +func handleRecoverRedis(redisInfo *repo.RootInfo, recoverFile string) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(recoverFile) { + return errors.New(fmt.Sprintf("%s file is not exist", recoverFile)) + } + + appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") + if err != nil { + return err + } + global.LOG.Infof("appendonly in redis conf is %s", appendonly) + composeDir := fmt.Sprintf("%s/redis/%s", constant.AppInstallDir, redisInfo.Name) + if _, err := compose.Down(composeDir + "/docker-compose.yml"); err != nil { + return err + } + if appendonly == "yes" { + redisDataDir := fmt.Sprintf("%s/%s/%s/data/", constant.AppInstallDir, "redis", redisInfo.Name) + if err := handleUnTar(recoverFile, redisDataDir); err != nil { + return err + } + } else { + input, err := ioutil.ReadFile(recoverFile) + if err != nil { + return err + } + if err = ioutil.WriteFile(composeDir+"/data/dump.rdb", input, 0640); err != nil { + return err + } + } + if _, err := compose.Up(composeDir + "/docker-compose.yml"); err != nil { + return err + } + return nil +} diff --git a/backend/app/service/backup_website.go b/backend/app/service/backup_website.go new file mode 100644 index 000000000..3113c81de --- /dev/null +++ b/backend/app/service/backup_website.go @@ -0,0 +1,238 @@ +package service + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" + "github.com/1Panel-dev/1Panel/backend/utils/compose" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(req.Name)) + if err != nil { + return err + } + + timeNow := time.Now().Format("20060102150405") + backupDir := fmt.Sprintf("%s/website/%s", localDir, req.Name) + fileName := fmt.Sprintf("%s_%s.tar.gz", website.PrimaryDomain, timeNow) + if err := handleWebsiteBackup(&website, backupDir, fileName); err != nil { + return err + } + + record := &model.BackupRecord{ + Type: "website", + Name: website.PrimaryDomain, + DetailName: "", + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: backupDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + return nil +} + +func (u *BackupService) WebsiteRecoverByUpload(req dto.CommonRecover) error { + if err := handleUnTar(req.File, path.Dir(req.File)); err != nil { + return err + } + tmpDir := strings.ReplaceAll(req.File, ".tar.gz", "") + webJson, err := os.ReadFile(fmt.Sprintf("%s/website.json", tmpDir)) + if err != nil { + return err + } + var websiteInfo WebsiteInfo + if err := json.Unmarshal(webJson, &websiteInfo); err != nil { + return err + } + if websiteInfo.WebsiteName != req.Name { + return errors.New("the uploaded file does not match the selected website and cannot be recovered") + } + + website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(req.Name)) + if err != nil { + return err + } + if err := handleWebsiteRecover(&website, tmpDir); err != nil { + return err + } + + return nil +} + +func (u *BackupService) WebsiteRecover(req dto.CommonRecover) error { + website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(req.Name)) + if err != nil { + return err + } + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return errors.New(fmt.Sprintf("%s file is not exist", req.File)) + } + global.LOG.Infof("recover website %s from backup file %s", req.Name, req.File) + if err := handleWebsiteRecover(&website, req.File); err != nil { + return err + } + return nil +} + +func handleWebsiteRecover(website *model.Website, recoverFile string) error { + fileOp := files.NewFileOp() + fileDir := strings.ReplaceAll(recoverFile, ".tar.gz", "") + if err := fileOp.Decompress(recoverFile, path.Dir(recoverFile), files.TarGz); err != nil { + return err + } + defer func() { + _ = os.RemoveAll(fileDir) + }() + + itemDir := fmt.Sprintf("%s/%s", fileDir, website.Alias) + if !fileOp.Stat(itemDir+".conf") || !fileOp.Stat(itemDir+".web.tar.gz") { + return errors.New("the wrong recovery package does not have .conf or .web.tar.gz files") + } + if website.Type == constant.Deployment { + if !fileOp.Stat(itemDir+".sql.gz") || !fileOp.Stat(itemDir+".app.tar.gz") { + return errors.New("the wrong recovery package does not have .sql.gz or .app.tar.gz files") + } + } + + nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") + if err != nil { + return err + } + nginxConfPath := fmt.Sprintf("%s/openresty/%s/conf/conf.d", constant.AppInstallDir, nginxInfo.Name) + if err := fileOp.CopyFile(fmt.Sprintf("%s/%s.conf", fileDir, website.Alias), nginxConfPath); err != nil { + return err + } + + if website.Type == constant.Deployment { + mysqlInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMysql, "") + if err != nil { + return err + } + resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) + if err != nil { + return err + } + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + if err := handleMysqlRecover(mysqlInfo, fileDir, db.Name, fmt.Sprintf("%s.sql.gz", website.Alias)); err != nil { + return err + } + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if err := handleAppRecover(&app, fmt.Sprintf("%s/%s.app.tar.gz", fileDir, website.Alias)); err != nil { + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, app.App.Key, app.Name)); err != nil { + return err + } + } + siteDir := fmt.Sprintf("%s/openresty/%s/www/sites", constant.AppInstallDir, nginxInfo.Name) + if err := fileOp.Decompress(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.Alias), siteDir, files.TarGz); err != nil { + return err + } + stdout, err := cmd.Execf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName) + if err != nil { + return errors.New(string(stdout)) + } + + return nil +} + +type WebsiteInfo struct { + WebsiteName string `json:"websiteName"` + WebsiteType string `json:"websiteType"` +} + +func handleWebsiteBackup(website *model.Website, backupDir, fileName string) error { + fileOp := files.NewFileOp() + tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) + if !fileOp.Stat(tmpDir) { + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + var websiteInfo WebsiteInfo + websiteInfo.WebsiteType = website.Type + websiteInfo.WebsiteName = website.PrimaryDomain + remarkInfo, _ := json.Marshal(websiteInfo) + if err := fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil { + return err + } + global.LOG.Info("put websitejson into tmp dir successful") + + nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") + if err != nil { + return err + } + nginxConfFile := fmt.Sprintf("%s/openresty/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.Alias) + if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil { + return err + } + global.LOG.Info("put openresty conf into tmp dir successful") + + if website.Type == constant.Deployment { + mysqlInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMysql, "") + if err != nil { + return err + } + resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) + if err != nil { + return err + } + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + if err := handleMysqlBackup(mysqlInfo, tmpDir, db.Name, fmt.Sprintf("%s.sql.gz", website.Alias)); err != nil { + return err + } + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if err := handleAppBackup(&app, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias)); err != nil { + return err + } + global.LOG.Info("put app tar into tmp dir successful") + } + websiteDir := fmt.Sprintf("%s/openresty/%s/www/sites/%s", constant.AppInstallDir, nginxInfo.Name, website.Alias) + if err := fileOp.Compress([]string{websiteDir}, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias), files.TarGz); err != nil { + return err + } + global.LOG.Info("put website tar into tmp dir successful, now start to tar tmp dir") + if err := fileOp.Compress([]string{tmpDir}, backupDir, fileName, files.TarGz); err != nil { + return err + } + + return nil +} diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index 54a887f29..94f278f30 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -70,25 +70,19 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Time) (string, error) { var ( - baseDir string backupDir string fileName string + record model.BackupRecord ) backup, err := backupRepo.Get(commonRepo.WithByID(uint(cronjob.TargetDirID))) if err != nil { return "", err } - - global.LOG.Infof("start to backup %s %s to %s", cronjob.Type, cronjob.Name, backup.Type) - if cronjob.KeepLocal || cronjob.Type != "LOCAL" { - localDir, err := loadLocalDir() - if err != nil { - return "", err - } - baseDir = localDir - } else { - baseDir = global.CONF.System.TmpDir + localDir, err := loadLocalDir() + if err != nil { + return "", err } + global.LOG.Infof("start to backup %s %s to %s", cronjob.Type, cronjob.Name, backup.Type) switch cronjob.Type { case "database": @@ -97,44 +91,68 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim return "", err } fileName = fmt.Sprintf("db_%s_%s.sql.gz", cronjob.DBName, startTime.Format("20060102150405")) - backupDir = fmt.Sprintf("database/mysql/%s/%s", app.Name, cronjob.DBName) - if err = backupMysql(backup.Type, baseDir, backupDir, app.Name, cronjob.DBName, fileName); err != nil { + backupDir = fmt.Sprintf("%s/database/mysql/%s/%s", localDir, app.Name, cronjob.DBName) + if err = handleMysqlBackup(app, backupDir, cronjob.DBName, fileName); err != nil { return "", err } + record.Type = "mysql" + record.Name = "mysql" + record.DetailName = app.Name case "website": - fileName = fmt.Sprintf("website_%s_%s", cronjob.Website, startTime.Format("20060102150405")) - backupDir = fmt.Sprintf("website/%s", cronjob.Website) - if err := handleWebsiteBackup(backup.Type, baseDir, backupDir, cronjob.Website, fileName); err != nil { + fileName = fmt.Sprintf("website_%s_%s.tar.gz", cronjob.Website, startTime.Format("20060102150405")) + backupDir = fmt.Sprintf("%s/website/%s", localDir, cronjob.Website) + website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(cronjob.Website)) + if err != nil { return "", err } - fileName = fileName + ".tar.gz" + if err := handleWebsiteBackup(&website, backupDir, fileName); err != nil { + return "", err + } + record.Type = "website" + record.Name = website.PrimaryDomain default: fileName = fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format("20060102150405")) - backupDir = fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name) + backupDir = fmt.Sprintf("%s/%s/%s", localDir, cronjob.Type, cronjob.Name) global.LOG.Infof("handle tar %s to %s", backupDir, fileName) - if err := handleTar(cronjob.SourceDir, baseDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil { + if err := handleTar(cronjob.SourceDir, localDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil { + return "", err + } + } + if len(record.Name) != 0 { + record.FileName = fileName + record.FileDir = backupDir + record.Source = "LOCAL" + record.BackupType = backup.Type + if !cronjob.KeepLocal && backup.Type != "LOCAL" { + record.Source = backup.Type + record.FileDir = strings.ReplaceAll(backupDir, localDir+"/", "") + } + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) return "", err } } + fullPath := fmt.Sprintf("%s/%s", record.FileDir, fileName) if backup.Type == "LOCAL" { - u.HandleRmExpired(backup.Type, baseDir, backupDir, cronjob, nil) - return baseDir + "/" + backupDir + "/" + fileName, nil + u.HandleRmExpired(backup.Type, record.FileDir, cronjob, nil) + return fullPath, nil } - cloudFile := baseDir + "/" + backupDir + "/" + fileName if !cronjob.KeepLocal { - cloudFile = backupDir + "/" + fileName + defer func() { + _ = os.RemoveAll(fmt.Sprintf("%s/%s", backupDir, fileName)) + }() } client, err := NewIBackupService().NewClient(&backup) if err != nil { - return cloudFile, err + return fullPath, err } - if _, err = client.Upload(baseDir+"/"+backupDir+"/"+fileName, backupDir+"/"+fileName); err != nil { - return cloudFile, err + if _, err = client.Upload(backupDir+"/"+fileName, fullPath); err != nil { + return fullPath, err } - u.HandleRmExpired(backup.Type, baseDir, backupDir, cronjob, client) - return cloudFile, nil + u.HandleRmExpired(backup.Type, backupDir, cronjob, client) + return fullPath, nil } func (u *CronjobService) HandleDelete(id uint) error { @@ -156,7 +174,7 @@ func (u *CronjobService) HandleDelete(id uint) error { return nil } -func (u *CronjobService) HandleRmExpired(backType, baseDir, backupDir string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) { +func (u *CronjobService) HandleRmExpired(backType, backupDir string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) { global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) if backType != "LOCAL" { currentObjs, err := backClient.ListObjects(backupDir + "/") @@ -171,9 +189,9 @@ func (u *CronjobService) HandleRmExpired(backType, baseDir, backupDir string, cr return } } - files, err := ioutil.ReadDir(baseDir + "/" + backupDir) + files, err := ioutil.ReadDir(backupDir) if err != nil { - global.LOG.Errorf("read dir %s failed, err: %v", baseDir+"/"+backupDir, err) + global.LOG.Errorf("read dir %s failed, err: %v", backupDir, err) return } if len(files) == 0 { @@ -185,14 +203,14 @@ func (u *CronjobService) HandleRmExpired(backType, baseDir, backupDir string, cr if strings.HasPrefix(files[i].Name(), "db_") { dbCopies++ if dbCopies > cronjob.RetainCopies { - _ = os.Remove(baseDir + "/" + backupDir + "/" + files[i].Name()) + _ = os.Remove(backupDir + "/" + files[i].Name()) _ = backupRepo.DeleteRecord(context.Background(), backupRepo.WithByFileName(files[i].Name())) } } } } else { for i := 0; i < len(files)-int(cronjob.RetainCopies); i++ { - _ = os.Remove(baseDir + "/" + backupDir + "/" + files[i].Name()) + _ = os.Remove(backupDir + "/" + files[i].Name()) } } records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID))) diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 7fc75a077..b96b6093c 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -2,14 +2,12 @@ package service import ( "bufio" - "compress/gzip" "context" "encoding/json" "fmt" "io/ioutil" "os" "os/exec" - "path/filepath" "regexp" "strconv" "strings" @@ -21,7 +19,6 @@ import ( "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/files" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/copier" "github.com/pkg/errors" @@ -38,11 +35,6 @@ type IMysqlService interface { UpdateVariables(updatas []dto.MysqlVariablesUpdate) error UpdateConfByFile(info dto.MysqlConfUpdateByFile) error UpdateDescription(req dto.UpdateDescription) error - - RecoverByUpload(req dto.UploadRecover) error - Backup(db dto.BackupDB) error - Recover(db dto.RecoverDB) error - DeleteCheck(id uint) ([]string, error) Delete(ctx context.Context, req dto.MysqlDBDelete) error LoadStatus() (*dto.MysqlStatus, error) @@ -68,83 +60,6 @@ func (u *MysqlService) SearchWithPage(search dto.SearchWithPage) (int64, interfa return total, dtoMysqls, err } -func (u *MysqlService) RecoverByUpload(req dto.UploadRecover) error { - app, err := appInstallRepo.LoadBaseInfo("mysql", "") - if err != nil { - return err - } - file := req.FileDir + "/" + req.FileName - if !strings.HasSuffix(req.FileName, ".sql") && !strings.HasSuffix(req.FileName, ".gz") { - fileOp := files.NewFileOp() - fileNameItem := time.Now().Format("20060102150405") - dstDir := fmt.Sprintf("%s/%s", req.FileDir, fileNameItem) - if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { - if err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) - } - } - } - var compressType files.CompressType - switch { - case strings.HasSuffix(req.FileName, ".tar.gz"), strings.HasSuffix(req.FileName, ".tgz"): - compressType = files.TarGz - case strings.HasSuffix(req.FileName, ".zip"): - compressType = files.Zip - } - if err := fileOp.Decompress(req.FileDir+"/"+req.FileName, dstDir, compressType); err != nil { - _ = os.RemoveAll(dstDir) - return err - } - global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.FileDir+"/"+req.FileName) - hasTestSql := false - _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && info.Name() == "test.sql" { - hasTestSql = true - file = path - } - return nil - }) - if !hasTestSql { - _ = os.RemoveAll(dstDir) - return fmt.Errorf("no such file named test.sql in %s, err: %v", req.FileName, err) - } - defer func() { - _ = os.RemoveAll(dstDir) - }() - } - - global.LOG.Info("start to do recover from uploads") - fi, _ := os.Open(file) - defer fi.Close() - cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, req.DBName) - if strings.HasSuffix(req.FileName, ".gz") { - gzipFile, err := os.Open(file) - if err != nil { - return err - } - defer gzipFile.Close() - gzipReader, err := gzip.NewReader(gzipFile) - if err != nil { - return err - } - defer gzipReader.Close() - cmd.Stdin = gzipReader - } else { - cmd.Stdin = fi - } - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return errors.New(stdStr) - } - global.LOG.Info("recover from uploads successful!") - return nil -} - func (u *MysqlService) ListDBName() ([]string, error) { mysqls, err := mysqlRepo.List() var dbNames []string @@ -205,46 +120,6 @@ func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error { return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) } -func (u *MysqlService) Backup(db dto.BackupDB) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - backupDir := fmt.Sprintf("database/mysql/%s/%s", db.MysqlName, db.DBName) - fileName := fmt.Sprintf("%s_%s.sql.gz", db.DBName, time.Now().Format("20060102150405")) - if err := backupMysql("LOCAL", localDir, backupDir, db.MysqlName, db.DBName, fileName); err != nil { - return err - } - return nil -} - -func (u *MysqlService) Recover(req dto.RecoverDB) error { - app, err := appInstallRepo.LoadBaseInfo("mysql", "") - if err != nil { - return err - } - - global.LOG.Infof("recover database %s-%s from backup file %s", req.MysqlName, req.DBName, req.BackupName) - gzipFile, err := os.Open(req.BackupName) - if err != nil { - return err - } - defer gzipFile.Close() - gzipReader, err := gzip.NewReader(gzipFile) - if err != nil { - return err - } - defer gzipReader.Close() - cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, req.DBName) - cmd.Stdin = gzipReader - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return errors.New(stdStr) - } - return nil -} - func (u *MysqlService) DeleteCheck(id uint) ([]string, error) { var appInUsed []string app, err := appInstallRepo.LoadBaseInfo("mysql", "") @@ -301,7 +176,7 @@ func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error } global.LOG.Infof("delete database %s-%s backups successful", app.Name, db.Name) } - _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType("database-mysql"), commonRepo.WithByName(app.Name), backupRepo.WithByDetailName(db.Name)) + _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType("mysql"), commonRepo.WithByName(app.Name), backupRepo.WithByDetailName(db.Name)) _ = mysqlRepo.Delete(ctx, commonRepo.WithByID(db.ID)) return nil @@ -626,49 +501,6 @@ func excuteSql(containerName, password, command string) error { return nil } -func backupMysql(backupType, baseDir, backupDir, mysqlName, dbName, fileName string) error { - app, err := appInstallRepo.LoadBaseInfo("mysql", "") - if err != nil { - return err - } - - fullDir := baseDir + "/" + backupDir - if _, err := os.Stat(fullDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(fullDir, os.ModePerm); err != nil { - if err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", fullDir, err) - } - } - } - outfile, _ := os.OpenFile(fullDir+"/"+fileName, os.O_RDWR|os.O_CREATE, 0755) - global.LOG.Infof("start to mysqldump | gzip > %s.gzip", fullDir+"/"+fileName) - cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, dbName) - gzipCmd := exec.Command("gzip", "-cf") - gzipCmd.Stdin, _ = cmd.StdoutPipe() - gzipCmd.Stdout = outfile - _ = gzipCmd.Start() - _ = cmd.Run() - _ = gzipCmd.Wait() - - record := &model.BackupRecord{ - Type: "database-mysql", - Name: app.Name, - DetailName: dbName, - Source: backupType, - BackupType: backupType, - FileDir: backupDir, - FileName: fileName, - } - if baseDir != global.CONF.System.TmpDir || backupType == "LOCAL" { - record.Source = "LOCAL" - record.FileDir = fullDir - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } - return nil -} - func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string { isOn := false hasGroup := false diff --git a/backend/app/service/database_redis.go b/backend/app/service/database_redis.go index eae86b278..8e45fc92b 100644 --- a/backend/app/service/database_redis.go +++ b/backend/app/service/database_redis.go @@ -9,12 +9,9 @@ import ( "os/exec" "path/filepath" "strings" - "time" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/compose" _ "github.com/go-sql-driver/mysql" ) @@ -30,9 +27,7 @@ type IRedisService interface { LoadConf() (*dto.RedisConf, error) LoadPersistenceConf() (*dto.RedisPersistence, error) - Backup() error SearchBackupListWithPage(req dto.PageInfo) (int64, interface{}, error) - Recover(req dto.RedisBackupRecover) error } func NewIRedisService() IRedisService { @@ -156,89 +151,6 @@ func (u *RedisService) LoadPersistenceConf() (*dto.RedisPersistence, error) { return &item, nil } -func (u *RedisService) Backup() error { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", "") - if err != nil { - return err - } - stdout, err := cmd.Execf("docker exec %s redis-cli -a %s --no-auth-warning save", redisInfo.ContainerName, redisInfo.Password) - if err != nil { - return errors.New(string(stdout)) - } - localDir, err := loadLocalDir() - if err != nil { - return err - } - backupDir := fmt.Sprintf("database/redis/%s/", redisInfo.Name) - fullDir := fmt.Sprintf("%s/%s", localDir, backupDir) - if _, err := os.Stat(fullDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(fullDir, os.ModePerm); err != nil { - if err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", fullDir, err) - } - } - } - - appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") - if err != nil { - return err - } - - global.LOG.Infof("appendonly in redis conf is %s", appendonly) - if appendonly == "yes" { - redisDataDir := fmt.Sprintf("%s/%s/%s/data", constant.AppInstallDir, "redis", redisInfo.Name) - name := fmt.Sprintf("%s.tar.gz", time.Now().Format("20060102150405")) - if err := handleTar(redisDataDir+"/appendonlydir", fullDir, name, ""); err != nil { - return err - } - return nil - } - - name := fmt.Sprintf("%s.rdb", time.Now().Format("20060102150405")) - stdout1, err1 := cmd.Execf("docker cp %s:/data/dump.rdb %s/%s", redisInfo.ContainerName, fullDir, name) - if err1 != nil { - return errors.New(string(stdout1)) - } - return nil -} - -func (u *RedisService) Recover(req dto.RedisBackupRecover) error { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", "") - if err != nil { - return err - } - appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") - if err != nil { - return err - } - global.LOG.Infof("appendonly in redis conf is %s", appendonly) - - composeDir := fmt.Sprintf("%s/redis/%s", constant.AppInstallDir, redisInfo.Name) - if _, err := compose.Down(composeDir + "/docker-compose.yml"); err != nil { - return err - } - fullName := fmt.Sprintf("%s/%s", req.FileDir, req.FileName) - if appendonly == "yes" { - redisDataDir := fmt.Sprintf("%s/%s/%s/data/", constant.AppInstallDir, "redis", redisInfo.Name) - if err := handleUnTar(fullName, redisDataDir); err != nil { - return err - } - } else { - input, err := ioutil.ReadFile(fullName) - if err != nil { - return err - } - if err = ioutil.WriteFile(composeDir+"/data/dump.rdb", input, 0640); err != nil { - return err - } - } - if _, err := compose.Up(composeDir + "/docker-compose.yml"); err != nil { - return err - } - - return nil -} - func (u *RedisService) SearchBackupListWithPage(req dto.PageInfo) (int64, interface{}, error) { var ( list []dto.DatabaseFileRecords diff --git a/backend/app/service/docker.go b/backend/app/service/docker.go index 36f83a448..5cf6693eb 100644 --- a/backend/app/service/docker.go +++ b/backend/app/service/docker.go @@ -104,9 +104,7 @@ func (u *DockerService) LoadDockerConf() *dto.DaemonJsonConf { func (u *DockerService) UpdateConf(req dto.DaemonJsonConf) error { if _, err := os.Stat(constant.DaemonJsonPath); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(path.Dir(constant.DaemonJsonPath), os.ModePerm); err != nil { - if err != nil { - return err - } + return err } _, _ = os.Create(constant.DaemonJsonPath) } diff --git a/backend/app/service/image_repo.go b/backend/app/service/image_repo.go index 6f440bdf1..693c3fd3f 100644 --- a/backend/app/service/image_repo.go +++ b/backend/app/service/image_repo.go @@ -184,9 +184,7 @@ func (u *ImageRepoService) CheckConn(host, user, password string) error { func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) error { if _, err := os.Stat(constant.DaemonJsonPath); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(path.Dir(constant.DaemonJsonPath), os.ModePerm); err != nil { - if err != nil { - return err - } + return err } _, _ = os.Create(constant.DaemonJsonPath) } diff --git a/backend/app/service/website.go b/backend/app/service/website.go index a9c36d4fe..96f28c8d4 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -3,19 +3,19 @@ package service import ( "context" "crypto/x509" - "encoding/json" "encoding/pem" "fmt" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" "os" "path" "reflect" "strings" "time" + "github.com/1Panel-dev/1Panel/backend/app/dto/request" + "github.com/1Panel-dev/1Panel/backend/app/dto/response" + "github.com/1Panel-dev/1Panel/backend/app/repo" + "github.com/1Panel-dev/1Panel/backend/buserr" + "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" @@ -33,9 +33,6 @@ type IWebsiteService interface { CreateWebsite(ctx context.Context, create request.WebsiteCreate) error OpWebsite(req request.WebsiteOp) error GetWebsiteOptions() ([]string, error) - Backup(id uint) error - Recover(req request.WebsiteRecover) error - RecoverByUpload(req request.WebsiteRecoverByFile) error UpdateWebsite(req request.WebsiteUpdate) error DeleteWebsite(req request.WebsiteDelete) error GetWebsite(id uint) (response.WebsiteDTO, error) @@ -198,74 +195,6 @@ func (w WebsiteService) GetWebsiteOptions() ([]string, error) { return datas, nil } -func (w WebsiteService) Backup(id uint) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return err - } - - fileName := fmt.Sprintf("%s_%s", website.PrimaryDomain, time.Now().Format("20060102150405")) - backupDir := fmt.Sprintf("website/%s", website.PrimaryDomain) - if err := handleWebsiteBackup("LOCAL", localDir, backupDir, website.PrimaryDomain, fileName); err != nil { - return err - } - return nil -} - -func (w WebsiteService) RecoverByUpload(req request.WebsiteRecoverByFile) error { - if err := handleUnTar(fmt.Sprintf("%s/%s", req.FileDir, req.FileName), req.FileDir); err != nil { - return err - } - tmpDir := fmt.Sprintf("%s/%s", req.FileDir, strings.ReplaceAll(req.FileName, ".tar.gz", "")) - webJson, err := os.ReadFile(fmt.Sprintf("%s/website.json", tmpDir)) - if err != nil { - return err - } - var websiteInfo WebsiteInfo - if err := json.Unmarshal(webJson, &websiteInfo); err != nil { - return err - } - if websiteInfo.WebsiteName != req.WebsiteName || websiteInfo.WebsiteType != req.Type { - return errors.New("上传文件与选中网站不匹配,无法恢复") - } - - website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(req.WebsiteName)) - if err != nil { - return err - } - if err := handleWebsiteRecover(&website, tmpDir); err != nil { - return err - } - - return nil -} - -func (w WebsiteService) Recover(req request.WebsiteRecover) error { - website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(req.WebsiteName)) - if err != nil { - return err - } - - if !strings.Contains(req.BackupName, "/") { - return errors.New("error path of request") - } - fileDir := path.Dir(req.BackupName) - pathName := strings.ReplaceAll(path.Base(req.BackupName), ".tar.gz", "") - if err := handleUnTar(req.BackupName, fileDir); err != nil { - return err - } - fileDir = fileDir + "/" + pathName - - if err := handleWebsiteRecover(&website, fileDir); err != nil { - return err - } - return nil -} - func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error { website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go index 9aca634e5..08a8b3d27 100644 --- a/backend/app/service/website_utils.go +++ b/backend/app/service/website_utils.go @@ -1,11 +1,7 @@ package service import ( - "bufio" - "encoding/json" "fmt" - "os" - "os/exec" "path" "strconv" "strings" @@ -16,9 +12,6 @@ import ( "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/compose" "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/nginx" "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" @@ -391,180 +384,6 @@ func toMapStr(m map[string]interface{}) map[string]string { return ret } -type WebsiteInfo struct { - WebsiteName string `json:"websiteName"` - WebsiteType string `json:"websiteType"` -} - -func handleWebsiteBackup(backupType, baseDir, backupDir, domain, backupName string) error { - website, err := websiteRepo.GetFirst(websiteRepo.WithDomain(domain)) - if err != nil { - return err - } - - tmpDir := fmt.Sprintf("%s/%s/%s", baseDir, backupDir, backupName) - if _, err := os.Stat(tmpDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(tmpDir, os.ModePerm); err != nil { - if err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", tmpDir, err) - } - } - } - - global.LOG.Infof("make a tmp dir %s for website files successful", tmpDir) - if err := saveWebsiteJson(&website, tmpDir); err != nil { - return err - } - global.LOG.Info("put website into tmp dir successful") - - nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") - if err != nil { - return err - } - nginxConfFile := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.Alias) - fileOp := files.NewFileOp() - if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil { - return err - } - global.LOG.Info("put nginx conf into tmp dir successful") - - if website.Type == constant.Deployment { - if err := mysqlOperation(&website, "backup", tmpDir); err != nil { - return err - } - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - websiteDir := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, app.App.Key, app.Name) - if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), ""); err != nil { - return err - } - global.LOG.Info("put app tar into tmp dir successful") - } - websiteDir := path.Join(constant.AppInstallDir, "nginx", nginxInfo.Name, "www", "sites", website.Alias) - if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias), ""); err != nil { - return err - } - global.LOG.Info("put website tar into tmp dir successful, now start to tar tmp dir") - if err := handleTar(tmpDir, fmt.Sprintf("%s/%s", baseDir, backupDir), backupName+".tar.gz", ""); err != nil { - return err - } - _ = os.RemoveAll(tmpDir) - - record := &model.BackupRecord{ - Type: "website-" + website.Type, - Name: website.PrimaryDomain, - DetailName: "", - Source: backupType, - BackupType: backupType, - FileDir: backupDir, - FileName: fmt.Sprintf("%s.tar.gz", backupName), - } - if baseDir != global.CONF.System.TmpDir || backupType == "LOCAL" { - record.Source = "LOCAL" - record.FileDir = fmt.Sprintf("%s/%s", baseDir, backupDir) - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } - return nil -} - -func handleWebsiteRecover(website *model.Website, fileDir string) error { - nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") - if err != nil { - return err - } - nginxConfPath := fmt.Sprintf("%s/nginx/%s/conf/conf.d", constant.AppInstallDir, nginxInfo.Name) - if err := files.NewFileOp().CopyFile(path.Join(fileDir, website.Alias+".conf"), nginxConfPath); err != nil { - return err - } - - if website.Type == constant.Deployment { - if err := mysqlOperation(website, "recover", fileDir); err != nil { - return err - } - - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - appDir := fmt.Sprintf("%s/%s", constant.AppInstallDir, app.App.Key) - if err := handleUnTar(fmt.Sprintf("%s/%s.app.tar.gz", fileDir, website.Alias), appDir); err != nil { - return err - } - if _, err := compose.Restart(fmt.Sprintf("%s/%s/docker-compose.yml", appDir, app.Name)); err != nil { - return err - } - } - siteDir := fmt.Sprintf("%s/nginx/%s/www/sites", constant.AppInstallDir, nginxInfo.Name) - if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.Alias), siteDir); err != nil { - return err - } - stdout, err := cmd.Execf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName) - if err != nil { - return errors.New(string(stdout)) - } - _ = os.RemoveAll(fileDir) - - return nil -} - -func mysqlOperation(website *model.Website, operation, filePath string) error { - mysqlInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMysql, "") - if err != nil { - return err - } - resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) - if err != nil { - return err - } - db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err - } - if operation == "backup" { - dbFile := fmt.Sprintf("%s/%s.sql", filePath, website.PrimaryDomain) - outfile, _ := os.OpenFile(dbFile, os.O_RDWR|os.O_CREATE, 0755) - defer outfile.Close() - cmd := exec.Command("docker", "exec", mysqlInfo.ContainerName, "mysqldump", "-uroot", "-p"+mysqlInfo.Password, db.Name) - cmd.Stdout = outfile - _ = cmd.Run() - _ = cmd.Wait() - return nil - } - cmd := exec.Command("docker", "exec", "-i", mysqlInfo.ContainerName, "mysql", "-uroot", "-p"+mysqlInfo.Password, db.Name) - sqlfile, err := os.Open(fmt.Sprintf("%s/%s.sql", filePath, website.Alias)) - if err != nil { - return err - } - defer sqlfile.Close() - cmd.Stdin = sqlfile - stdout, err := cmd.CombinedOutput() - if err != nil { - return errors.New(string(stdout)) - } - return nil -} - -func saveWebsiteJson(website *model.Website, tmpDir string) error { - var websiteInfo WebsiteInfo - websiteInfo.WebsiteType = website.Type - websiteInfo.WebsiteName = website.PrimaryDomain - remarkInfo, _ := json.Marshal(websiteInfo) - path := fmt.Sprintf("%s/website.json", tmpDir) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(string(remarkInfo)) - write.Flush() - return nil -} - func deleteWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) error { nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go index 3f4a3099c..8e2b9b7e6 100644 --- a/backend/router/ro_database.go +++ b/backend/router/ro_database.go @@ -19,9 +19,6 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) { cmdRouter.POST("", baseApi.CreateMysql) cmdRouter.POST("/change/access", baseApi.ChangeMysqlAccess) cmdRouter.POST("/change/password", baseApi.ChangeMysqlPassword) - cmdRouter.POST("/backup", baseApi.BackupMysql) - cmdRouter.POST("/recover/byupload", baseApi.RecoverMysqlByUpload) - cmdRouter.POST("/recover", baseApi.RecoverMysql) cmdRouter.POST("/del/check", baseApi.DeleteCheckMysql) cmdRouter.POST("/del", baseApi.DeleteMysql) cmdRouter.POST("/description/update", baseApi.UpdateMysqlDescription) @@ -39,8 +36,6 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) { cmdRouter.GET("/redis/conf", baseApi.LoadRedisConf) cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh) cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) - cmdRouter.POST("/redis/backup", baseApi.RedisBackup) - cmdRouter.POST("/redis/recover", baseApi.RedisRecover) cmdRouter.POST("/redis/backup/search", baseApi.RedisBackupList) cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf) cmdRouter.POST("/redis/conffile/update", baseApi.UpdateRedisConfByFile) diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index 7d4f7e33f..373da39c3 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -35,6 +35,9 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription) settingRouter.GET("/backup/search", baseApi.ListBackup) + settingRouter.POST("/backup/backup", baseApi.Backup) + settingRouter.POST("/backup/recover", baseApi.Recover) + settingRouter.POST("/backup/recover/byupload", baseApi.RecoverByUpload) settingRouter.POST("/backup/search/files", baseApi.LoadFilesFromBackup) settingRouter.POST("/backup/buckets", baseApi.ListBuckets) settingRouter.POST("/backup", baseApi.CreateBackup) diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index 52eef1b57..75faf9927 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -25,9 +25,6 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { groupRouter.POST("/update", baseApi.UpdateWebsite) groupRouter.GET("/:id", baseApi.GetWebsite) groupRouter.POST("/del", baseApi.DeleteWebsite) - groupRouter.POST("/backup", baseApi.BackupWebsite) - groupRouter.POST("/recover", baseApi.RecoverWebsite) - groupRouter.POST("/recover/byupload", baseApi.RecoverWebsiteByUpload) groupRouter.POST("/default/server", baseApi.ChangeDefaultServer) groupRouter.GET("/domains/:websiteId", baseApi.GetWebDomains) diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index 4ce168bba..bfac51d84 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -1,6 +1,7 @@ package files import ( + "bufio" "context" "encoding/json" "fmt" @@ -84,6 +85,21 @@ func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error { return nil } +func (f FileOp) SaveFile(dst string, content string, mode fs.FileMode) error { + if !f.Stat(path.Dir(dst)) { + _ = f.CreateDir(path.Dir(dst), mode.Perm()) + } + file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(string(content)) + write.Flush() + return nil +} + func (f FileOp) Chmod(dst string, mode fs.FileMode) error { return f.Fs.Chmod(dst, mode) } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index ebd5165b1..232d17bda 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -8,6 +8,7 @@ declare module 'vue' { AppLayout: typeof import('./src/components/app-layout/index.vue')['default'] AppStatus: typeof import('./src/components/app-status/index.vue')['default'] BackButton: typeof import('./src/components/back-button/index.vue')['default'] + Backup: typeof import('./src/components/backup/index.vue')['default'] BreadCrumbs: typeof import('./src/components/bread-crumbs/index.vue')['default'] BreadCrumbsItem: typeof import('./src/components/bread-crumbs/bread-crumbs-item.vue')['default'] CardWithHeader: typeof import('./src/components/card-with-header/index.vue')['default'] diff --git a/frontend/src/api/interface/backup.ts b/frontend/src/api/interface/backup.ts index 52db5d5fa..8e4417d0e 100644 --- a/frontend/src/api/interface/backup.ts +++ b/frontend/src/api/interface/backup.ts @@ -43,4 +43,15 @@ export namespace Backup { name: string; detailName: string; } + export interface Backup { + type: string; + name: string; + detailName: string; + } + export interface Recover { + type: string; + name: string; + detailName: string; + file: string; + } } diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index 648a211c7..ad8247a85 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -5,10 +5,6 @@ export namespace Database { mysqlName: string; dbName: string; } - export interface Backup { - mysqlName: string; - dbName: string; - } export interface Recover { mysqlName: string; dbName: string; diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index 9c5d337de..371a5a1f0 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -6,16 +6,6 @@ export const searchMysqlDBs = (params: SearchWithPage) => { return http.post>(`/databases/search`, params); }; -export const backup = (params: Database.Backup) => { - return http.post(`/databases/backup`, params); -}; -export const recover = (params: Database.Recover) => { - return http.post(`/databases/recover`, params); -}; -export const recoverByUpload = (params: Database.RecoverByUpload) => { - return http.post(`/databases/recover/byupload`, params); -}; - export const addMysqlDB = (params: Database.MysqlDBCreate) => { return http.post(`/databases`, params); }; @@ -79,9 +69,6 @@ export const updateRedisConf = (params: Database.RedisConfUpdate) => { export const updateRedisConfByFile = (params: Database.RedisConfUpdateByFile) => { return http.post(`/databases/redis/conffile/update`, params); }; -export const backupRedis = () => { - return http.post(`/databases/redis/backup`); -}; export const recoverRedis = (param: Database.RedisRecover) => { return http.post(`/databases/redis/recover`, param); }; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 21adf0ae2..c1371e03e 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -51,6 +51,15 @@ export const loadBaseDir = () => { }; // backup +export const handleBackup = (params: Backup.Backup) => { + return http.post(`/settings/backup/backup`, params); +}; +export const handleRecover = (params: Backup.Recover) => { + return http.post(`/settings/backup/recover`, params); +}; +export const handleRecoverByUpload = (params: Backup.Recover) => { + return http.post(`/settings/backup/recover/byupload`, params); +}; export const getBackupList = () => { return http.get>(`/settings/backup/search`); }; diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 99da7e142..0af2005da 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -23,18 +23,6 @@ export const OpWebsiteLog = (req: Website.WebSiteOpLog) => { return http.post(`/websites/log`, req); }; -export const BackupWebsite = (req: Website.BackupReq) => { - return http.post(`/websites/backup`, req); -}; - -export const RecoverWebsite = (req: Website.WebSiteRecover) => { - return http.post(`/websites/recover`, req); -}; - -export const RecoverWebsiteByUpload = (req: Website.WebsiteRecoverByUpload) => { - return http.post(`/websites/recover/byupload`, req); -}; - export const UpdateWebsite = (req: Website.WebSiteUpdateReq) => { return http.post(`/websites/update`, req); }; diff --git a/frontend/src/views/database/mysql/backup/index.vue b/frontend/src/components/backup/index.vue similarity index 77% rename from frontend/src/views/database/mysql/backup/index.vue rename to frontend/src/components/backup/index.vue index e39175eab..cd4ae0e93 100644 --- a/frontend/src/views/database/mysql/backup/index.vue +++ b/frontend/src/components/backup/index.vue @@ -1,10 +1,22 @@