fix: 统一备份接口和前端组件封装

This commit is contained in:
ssongliu 2023-02-21 19:06:24 +08:00 committed by ssongliu
parent f7749af999
commit aa2bb73199
40 changed files with 1064 additions and 1362 deletions

View File

@ -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)
}

View File

@ -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 数据库删除前检查

View File

@ -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 备份记录分页

View File

@ -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

View File

@ -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 删除网站

View File

@ -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"`

View File

@ -2,6 +2,7 @@ package repo
import (
"context"
"github.com/1Panel-dev/1Panel/backend/app/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"

View File

@ -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:

View File

@ -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)

View File

@ -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

View 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
}

View 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
}

View 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
}

View 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
}

View File

@ -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)))

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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']

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
};

View File

@ -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`);
};

View File

@ -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);
};

View File

@ -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'));

View File

@ -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>

View File

@ -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);
};

View File

@ -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);
};

View File

@ -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 = () => {

View File

@ -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();

View File

@ -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>

View File

@ -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);
},

View File

@ -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'));