mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-24 11:09:16 +08:00
fix: 统一备份接口和前端组件封装
This commit is contained in:
parent
f7749af999
commit
aa2bb73199
@ -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)
|
||||
}
|
||||
|
@ -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 数据库删除前检查
|
||||
|
@ -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 备份记录分页
|
||||
|
@ -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
|
||||
|
@ -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 删除网站
|
||||
|
@ -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"`
|
||||
|
@ -2,6 +2,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
199
backend/app/service/backup_app.go
Normal file
199
backend/app/service/backup_app.go
Normal file
@ -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
|
||||
}
|
170
backend/app/service/backup_database.go
Normal file
170
backend/app/service/backup_database.go
Normal file
@ -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
|
||||
}
|
132
backend/app/service/backup_redis.go
Normal file
132
backend/app/service/backup_redis.go
Normal file
@ -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
|
||||
}
|
238
backend/app/service/backup_website.go
Normal file
238
backend/app/service/backup_website.go
Normal file
@ -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
|
||||
}
|
@ -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)))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -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']
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -6,16 +6,6 @@ export const searchMysqlDBs = (params: SearchWithPage) => {
|
||||
return http.post<ResPage<Database.MysqlDBInfo>>(`/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);
|
||||
};
|
||||
|
@ -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<Array<Backup.BackupInfo>>(`/settings/backup/search`);
|
||||
};
|
||||
|
@ -23,18 +23,6 @@ export const OpWebsiteLog = (req: Website.WebSiteOpLog) => {
|
||||
return http.post<Website.WebSiteLog>(`/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<any>(`/websites/update`, req);
|
||||
};
|
||||
|
@ -1,10 +1,22 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div>
|
||||
<el-drawer v-model="backupVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('database.backup')" :resource="dbName" :back="handleClose" />
|
||||
<DrawerHeader
|
||||
v-if="detailName"
|
||||
:header="$t('database.backup')"
|
||||
:resource="name + '(' + detailName + ')'"
|
||||
:back="handleClose"
|
||||
/>
|
||||
<DrawerHeader v-else :header="$t('database.backup')" :resource="name" :back="handleClose" />
|
||||
</template>
|
||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
|
||||
<ComplexTable
|
||||
v-loading="loading"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button type="primary" @click="onBackup()">
|
||||
{{ $t('database.backup') }}
|
||||
@ -34,7 +46,7 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { backup, recover } from '@/api/modules/database';
|
||||
import { handleBackup, handleRecover } from '@/api/modules/setting';
|
||||
import i18n from '@/lang';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { deleteBackupRecord, downloadBackupRecord, searchBackupRecords } from '@/api/modules/setting';
|
||||
@ -52,15 +64,19 @@ const paginationConfig = reactive({
|
||||
});
|
||||
|
||||
const backupVisiable = ref(false);
|
||||
const mysqlName = ref();
|
||||
const dbName = ref();
|
||||
const type = ref();
|
||||
const name = ref();
|
||||
const detailName = ref();
|
||||
|
||||
interface DialogProps {
|
||||
mysqlName: string;
|
||||
dbName: string;
|
||||
type: string;
|
||||
name: string;
|
||||
detailName: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
mysqlName.value = params.mysqlName;
|
||||
dbName.value = params.dbName;
|
||||
type.value = params.type;
|
||||
name.value = params.name;
|
||||
detailName.value = params.detailName;
|
||||
backupVisiable.value = true;
|
||||
search();
|
||||
};
|
||||
@ -72,9 +88,9 @@ const search = async () => {
|
||||
let params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
type: 'database-mysql',
|
||||
name: mysqlName.value,
|
||||
detailName: dbName.value,
|
||||
type: type.value,
|
||||
name: name.value,
|
||||
detailName: detailName.value,
|
||||
};
|
||||
const res = await searchBackupRecords(params);
|
||||
data.value = res.data.items || [];
|
||||
@ -83,12 +99,14 @@ const search = async () => {
|
||||
|
||||
const onBackup = async () => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName.value,
|
||||
type: type.value,
|
||||
name: name.value,
|
||||
detailName: detailName.value,
|
||||
};
|
||||
loading.value = true;
|
||||
await backup(params)
|
||||
await handleBackup(params)
|
||||
.then(() => {
|
||||
console.log(loading.value);
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
@ -100,12 +118,13 @@ const onBackup = async () => {
|
||||
|
||||
const onRecover = async (row: Backup.RecordInfo) => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName.value,
|
||||
backupName: row.fileDir + '/' + row.fileName,
|
||||
type: type.value,
|
||||
name: name.value,
|
||||
detailName: detailName.value,
|
||||
file: row.fileDir + '/' + row.fileName,
|
||||
};
|
||||
loading.value = true;
|
||||
await recover(params)
|
||||
await handleRecover(params)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
@ -1,202 +0,0 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:close-on-click-modal="false"
|
||||
v-model="open"
|
||||
size="50%"
|
||||
:destroy-on-close="true"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<template #header>
|
||||
<Header :header="$t('app.backup')" :resource="installData.appInstallName" :back="handleClose"></Header>
|
||||
</template>
|
||||
<ComplexTable
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@search="search"
|
||||
v-loading="loading"
|
||||
v-model:selects="selects"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button type="primary" @click="backup">{{ $t('app.backup') }}</el-button>
|
||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete()">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table-column type="selection" fix />
|
||||
|
||||
<el-table-column
|
||||
:label="$t('app.backupName')"
|
||||
min-width="120px"
|
||||
prop="name"
|
||||
show-overflow-tooltip
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('app.backupPath')"
|
||||
min-width="120px"
|
||||
prop="path"
|
||||
show-overflow-tooltip
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('app.backupdate')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
width="300px"
|
||||
:ellipsis="10"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
<el-dialog
|
||||
v-model="openRestorePage"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:title="$t('commons.msg.operate')"
|
||||
width="30%"
|
||||
>
|
||||
<el-alert :title="$t('app.restoreWarn')" type="warning" :closable="false" show-icon />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="openRestorePage = false" :loading="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="restore" :loading="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="installBackup">
|
||||
import { DelAppBackups, GetAppBackups, InstalledOp } from '@/api/modules/app';
|
||||
import { reactive, ref } from 'vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import Header from '@/components/drawer-header/index.vue';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
interface InstallRrops {
|
||||
appInstallId: number;
|
||||
appInstallName: string;
|
||||
}
|
||||
const installData = ref<InstallRrops>({
|
||||
appInstallId: 0,
|
||||
appInstallName: '',
|
||||
});
|
||||
const selects = ref<any>([]);
|
||||
let open = ref(false);
|
||||
let loading = ref(false);
|
||||
let data = ref<any>();
|
||||
let openRestorePage = ref(false);
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
let req = reactive({
|
||||
installId: installData.value.appInstallId,
|
||||
operate: 'restore',
|
||||
backupId: -1,
|
||||
});
|
||||
|
||||
const em = defineEmits(['close']);
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
em('close', open);
|
||||
};
|
||||
|
||||
const acceptParams = (props: InstallRrops) => {
|
||||
installData.value.appInstallId = props.appInstallId;
|
||||
installData.value.appInstallName = props.appInstallName;
|
||||
search();
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const req = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
appInstallId: installData.value.appInstallId,
|
||||
};
|
||||
await GetAppBackups(req).then((res) => {
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
});
|
||||
};
|
||||
|
||||
const backup = async () => {
|
||||
const req = {
|
||||
installId: installData.value.appInstallId,
|
||||
operate: 'backup',
|
||||
};
|
||||
loading.value = true;
|
||||
await InstalledOp(req)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.backupSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openRestore = (backupId: number) => {
|
||||
openRestorePage.value = true;
|
||||
req.backupId = backupId;
|
||||
req.operate = 'restore';
|
||||
req.installId = installData.value.appInstallId;
|
||||
};
|
||||
|
||||
const restore = async () => {
|
||||
loading.value = true;
|
||||
await InstalledOp(req)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.restoreSuccess'));
|
||||
openRestorePage.value = false;
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteBackup = async (ids: number[]) => {
|
||||
await useDeleteData(DelAppBackups, { ids: ids }, 'commons.msg.delete');
|
||||
search();
|
||||
};
|
||||
|
||||
const onBatchDelete = () => {
|
||||
let ids: Array<number> = [];
|
||||
selects.value.forEach((item: any) => {
|
||||
ids.push(item.id);
|
||||
});
|
||||
deleteBackup(ids);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('app.delete'),
|
||||
click: (row: any) => {
|
||||
deleteBackup([row.id]);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.restore'),
|
||||
click: (row: any) => {
|
||||
openRestore(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -95,7 +95,7 @@
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="openBackups(installed.id, installed.name)"
|
||||
@click="openBackups(installed.app.key, installed.name)"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
{{ $t('app.backup') }}
|
||||
@ -163,7 +163,7 @@ import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import Backups from './backup/index.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import AppResources from './check/index.vue';
|
||||
import AppDelete from './delete/index.vue';
|
||||
import AppParams from './detail/index.vue';
|
||||
@ -335,10 +335,11 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
|
||||
const openBackups = (installId: number, installName: string) => {
|
||||
const openBackups = (key: string, name: string) => {
|
||||
let params = {
|
||||
appInstallId: installId,
|
||||
appInstallName: installName,
|
||||
type: 'app',
|
||||
name: key,
|
||||
detailName: name,
|
||||
};
|
||||
backupRef.value.acceptParams(params);
|
||||
};
|
||||
|
@ -140,7 +140,7 @@
|
||||
<RemoteAccessDialog ref="remoteAccessRef" />
|
||||
<UploadDialog ref="uploadRef" />
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<BackupRecords ref="dialogBackupRef" />
|
||||
<Backups ref="dialogBackupRef" />
|
||||
|
||||
<AppResources ref="checkRef"></AppResources>
|
||||
<DeleteDialog ref="deleteRef" @search="search" />
|
||||
@ -155,11 +155,11 @@ import DeleteDialog from '@/views/database/mysql/delete/index.vue';
|
||||
import PasswordDialog from '@/views/database/mysql/password/index.vue';
|
||||
import RootPasswordDialog from '@/views/database/mysql/root-password/index.vue';
|
||||
import RemoteAccessDialog from '@/views/database/mysql/remote/index.vue';
|
||||
import BackupRecords from '@/views/database/mysql/backup/index.vue';
|
||||
import UploadDialog from '@/views/database/mysql/upload/index.vue';
|
||||
import AppResources from '@/views/database/mysql/check/index.vue';
|
||||
import Setting from '@/views/database/mysql/setting/index.vue';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { deleteCheckMysqlDB, loadRemoteAccess, searchMysqlDBs, updateMysqlDescription } from '@/api/modules/database';
|
||||
@ -205,8 +205,9 @@ const onOpenDialog = async () => {
|
||||
const dialogBackupRef = ref();
|
||||
const onOpenBackupDialog = async (dbName: string) => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName,
|
||||
type: 'mysql',
|
||||
name: mysqlName.value,
|
||||
detailName: dbName,
|
||||
};
|
||||
dialogBackupRef.value!.acceptParams(params);
|
||||
};
|
||||
|
@ -65,7 +65,7 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computeSize, dateFormatSimple } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { recoverByUpload } from '@/api/modules/database';
|
||||
import { handleRecoverByUpload } from '@/api/modules/setting';
|
||||
import i18n from '@/lang';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus';
|
||||
import { File } from '@/api/interface/file';
|
||||
@ -116,13 +116,13 @@ const search = async () => {
|
||||
|
||||
const onRecover = async (row: File.File) => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName.value,
|
||||
fileDir: baseDir.value,
|
||||
fileName: row.name,
|
||||
type: 'mysql',
|
||||
name: mysqlName.value,
|
||||
detailName: dbName.value,
|
||||
file: baseDir.value + '/' + row.name,
|
||||
};
|
||||
loading.value = true;
|
||||
await recoverByUpload(params)
|
||||
await handleRecoverByUpload(params)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
@ -157,6 +157,7 @@ const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
|
||||
const handleClose = () => {
|
||||
uploadRef.value!.clearFiles();
|
||||
upVisiable.value = false;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
|
@ -119,12 +119,13 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import { Database } from '@/api/interface/database';
|
||||
import {
|
||||
backupRedis,
|
||||
recoverRedis,
|
||||
redisBackupRedisRecords,
|
||||
RedisPersistenceConf,
|
||||
updateRedisPersistenceConf,
|
||||
} from '@/api/modules/database';
|
||||
|
||||
import { handleBackup } from '@/api/modules/setting';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { FormInstance } from 'element-plus';
|
||||
@ -198,7 +199,7 @@ const loadBackupRecords = async () => {
|
||||
};
|
||||
const onBackup = async () => {
|
||||
loading.value = true;
|
||||
await backupRedis()
|
||||
await handleBackup({ name: '', detailName: '', type: 'redis' })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
loadBackupRecords();
|
||||
|
@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<el-drawer :close-on-click-modal="false" v-model="backupVisiable" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('database.backup')" :resource="websiteName" :back="handleClose"></DrawerHeader>
|
||||
</template>
|
||||
<ComplexTable
|
||||
v-loading="loading"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button type="primary" @click="onBackup()">
|
||||
{{ $t('database.backup') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" prop="fileName" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('database.source')" prop="backupType" />
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import i18n from '@/lang';
|
||||
import { deleteBackupRecord, downloadBackupRecord, searchBackupRecords } from '@/api/modules/setting';
|
||||
import { Backup } from '@/api/interface/backup';
|
||||
import { BackupWebsite, RecoverWebsite } from '@/api/modules/website';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const selects = ref<any>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const backupVisiable = ref(false);
|
||||
const websiteName = ref();
|
||||
const websiteID = ref();
|
||||
const websiteType = ref();
|
||||
|
||||
interface DialogProps {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
websiteName.value = params.name;
|
||||
websiteID.value = params.id;
|
||||
websiteType.value = params.type;
|
||||
backupVisiable.value = true;
|
||||
search();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
backupVisiable.value = false;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
let params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
type: 'website-' + websiteType.value,
|
||||
name: websiteName.value,
|
||||
detailName: '',
|
||||
};
|
||||
const res = await searchBackupRecords(params);
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const onRecover = async (row: Backup.RecordInfo) => {
|
||||
ElMessageBox.confirm(i18n.global.t('website.restoreHelper'), i18n.global.t('commons.button.recover'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(() => {
|
||||
recover(row);
|
||||
});
|
||||
};
|
||||
|
||||
const recover = async (row: Backup.RecordInfo) => {
|
||||
let params = {
|
||||
websiteName: websiteName.value,
|
||||
type: websiteType.value,
|
||||
backupName: row.fileDir + '/' + row.fileName,
|
||||
};
|
||||
loading.value = true;
|
||||
await RecoverWebsite(params)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onBackup = async () => {
|
||||
loading.value = true;
|
||||
await BackupWebsite({ id: websiteID.value })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onDownload = async (row: Backup.RecordInfo) => {
|
||||
let params = {
|
||||
source: row.source,
|
||||
fileDir: row.fileDir,
|
||||
fileName: row.fileName,
|
||||
};
|
||||
const res = await downloadBackupRecord(params);
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = row.fileName;
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
||||
let ids: Array<number> = [];
|
||||
if (row) {
|
||||
ids.push(row.id);
|
||||
} else {
|
||||
selects.value.forEach((item: Backup.RecordInfo) => {
|
||||
ids.push(item.id);
|
||||
});
|
||||
}
|
||||
await useDeleteData(deleteBackupRecord, { ids: ids }, 'commons.msg.delete');
|
||||
search();
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onBatchDelete(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onRecover(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.download'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onDownload(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -143,7 +143,7 @@
|
||||
<DeleteWebsite ref="deleteRef" @close="search" />
|
||||
<WebSiteGroup ref="groupRef" />
|
||||
<UploadDialog ref="uploadRef" />
|
||||
<BackupRecords ref="dialogBackupRef" />
|
||||
<Backups ref="dialogBackupRef" />
|
||||
<DefaultServer ref="defaultRef" />
|
||||
</div>
|
||||
</template>
|
||||
@ -151,7 +151,7 @@
|
||||
<script lang="ts" setup>
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import RouterButton from '@/components/router-button/index.vue';
|
||||
import BackupRecords from '@/views/website/website/backup/index.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import UploadDialog from '@/views/website/website/upload/index.vue';
|
||||
import DefaultServer from '@/views/website/website/default/index.vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
@ -312,9 +312,9 @@ const buttons = [
|
||||
label: i18n.global.t('database.backupList'),
|
||||
click: (row: Website.Website) => {
|
||||
let params = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
type: 'website',
|
||||
name: row.primaryDomain,
|
||||
detailName: '',
|
||||
};
|
||||
dialogBackupRef.value!.acceptParams(params);
|
||||
},
|
||||
|
@ -69,8 +69,7 @@ import i18n from '@/lang';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { BatchDeleteFile, GetFilesList, UploadFileData } from '@/api/modules/files';
|
||||
import { RecoverWebsiteByUpload } from '@/api/modules/website';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
import { handleRecoverByUpload, loadBaseDir } from '@/api/modules/setting';
|
||||
import Header from '@/components/drawer-header/index.vue';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
||||
@ -116,13 +115,13 @@ const search = async () => {
|
||||
|
||||
const onRecover = async (row: File.File) => {
|
||||
let params = {
|
||||
websiteName: websiteName.value,
|
||||
type: websiteType.value,
|
||||
fileDir: baseDir.value,
|
||||
fileName: row.name,
|
||||
name: websiteName.value,
|
||||
detailName: '',
|
||||
type: 'website',
|
||||
file: baseDir.value + '/' + row.name,
|
||||
};
|
||||
loading.value = true;
|
||||
await RecoverWebsiteByUpload(params)
|
||||
await handleRecoverByUpload(params)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
|
Loading…
Reference in New Issue
Block a user