mirror of https://github.com/1Panel-dev/1Panel
feat: 完成 mysql 从上传恢复功能
parent
c49d2ef243
commit
581c940336
|
@ -1,10 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
|
@ -118,21 +115,10 @@ func (b *BaseApi) UpdateMysqlConfByFile(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
mysqlInfo, err := mysqlService.LoadBaseInfo(req.MysqlName)
|
||||
if err != nil {
|
||||
if err := mysqlService.UpdateConfByFile(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
path := fmt.Sprintf("/opt/1Panel/data/apps/%s/%s/conf/my.cnf", mysqlInfo.MysqlKey, mysqlInfo.Name)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(req.File)
|
||||
write.Flush()
|
||||
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
@ -206,6 +192,21 @@ func (b *BaseApi) BackupMysql(c *gin.Context) {
|
|||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
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 := mysqlService.RecoverByUpload(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) RecoverMysql(c *gin.Context) {
|
||||
var req dto.RecoverDB
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
|
|
@ -131,6 +131,13 @@ type RecoverDB struct {
|
|||
BackupName string `json:"backupName" validate:"required"`
|
||||
}
|
||||
|
||||
type UploadRecover struct {
|
||||
MysqlName string `json:"mysqlName" validate:"required"`
|
||||
DBName string `json:"dbName" validate:"required"`
|
||||
FileName string `json:"fileName"`
|
||||
FileDir string `json:"fileDir"`
|
||||
}
|
||||
|
||||
// redis
|
||||
type RedisConfUpdate struct {
|
||||
Timeout string `json:"timeout"`
|
||||
|
@ -181,7 +188,7 @@ type RedisStatus struct {
|
|||
LatestForkUsec string `json:"latest_fork_usec"`
|
||||
}
|
||||
|
||||
type RedisBackupRecords struct {
|
||||
type DatabaseFileRecords struct {
|
||||
FileName string `json:"fileName"`
|
||||
FileDir string `json:"fileDir"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
|
|
|
@ -201,7 +201,11 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl
|
|||
return backClient, nil
|
||||
}
|
||||
|
||||
func loadLocalDir(backup model.BackupAccount) (string, error) {
|
||||
func loadLocalDir() (string, error) {
|
||||
backup, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
varMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -83,11 +83,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
|
|||
return "", err
|
||||
}
|
||||
if cronjob.KeepLocal || cronjob.Type != "LOCAL" {
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
localDir, err := loadLocalDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
|
@ -19,6 +21,7 @@ import (
|
|||
"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/go-sql-driver/mysql"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -33,8 +36,10 @@ type IMysqlService interface {
|
|||
Create(mysqlDto dto.MysqlDBCreate) error
|
||||
ChangeInfo(info dto.ChangeDBInfo) error
|
||||
UpdateVariables(mysqlName string, updatas []dto.MysqlVariablesUpdate) error
|
||||
UpdateConfByFile(info dto.MysqlConfUpdateByFile) error
|
||||
|
||||
UpFile(mysqlName string, files []*multipart.FileHeader) error
|
||||
RecoverByUpload(req dto.UploadRecover) error
|
||||
SearchUpListWithPage(req dto.SearchDBWithPage) (int64, interface{}, error)
|
||||
Backup(db dto.BackupDB) error
|
||||
Recover(db dto.RecoverDB) error
|
||||
|
@ -65,28 +70,23 @@ func (u *MysqlService) SearchWithPage(search dto.SearchDBWithPage) (int64, inter
|
|||
|
||||
func (u *MysqlService) SearchUpListWithPage(req dto.SearchDBWithPage) (int64, interface{}, error) {
|
||||
var (
|
||||
list []dto.RedisBackupRecords
|
||||
backDatas []dto.RedisBackupRecords
|
||||
list []dto.DatabaseFileRecords
|
||||
backDatas []dto.DatabaseFileRecords
|
||||
)
|
||||
redisInfo, err := mysqlRepo.LoadBaseInfoByName(req.MysqlName)
|
||||
localDir, appKey, err := loadBackupDirAndKey(req.MysqlName)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
return 0, list, nil
|
||||
}
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
uploadDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, redisInfo.Key, redisInfo.Name)
|
||||
uploadDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, appKey, req.MysqlName)
|
||||
if _, err := os.Stat(uploadDir); err != nil {
|
||||
return 0, list, nil
|
||||
}
|
||||
_ = filepath.Walk(uploadDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
list = append(list, dto.RedisBackupRecords{
|
||||
list = append(list, dto.DatabaseFileRecords{
|
||||
CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Size: int(info.Size()),
|
||||
FileDir: uploadDir,
|
||||
|
@ -97,7 +97,7 @@ func (u *MysqlService) SearchUpListWithPage(req dto.SearchDBWithPage) (int64, in
|
|||
})
|
||||
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
backDatas = make([]dto.RedisBackupRecords, 0)
|
||||
backDatas = make([]dto.DatabaseFileRecords, 0)
|
||||
} else {
|
||||
if end >= total {
|
||||
end = total
|
||||
|
@ -108,19 +108,11 @@ func (u *MysqlService) SearchUpListWithPage(req dto.SearchDBWithPage) (int64, in
|
|||
}
|
||||
|
||||
func (u *MysqlService) UpFile(mysqlName string, files []*multipart.FileHeader) error {
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
localDir, appKey, err := loadBackupDirAndKey(mysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(mysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, app.Key, mysqlName)
|
||||
dstDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, appKey, mysqlName)
|
||||
if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dstDir, os.ModePerm); err != nil {
|
||||
if err != nil {
|
||||
|
@ -139,6 +131,85 @@ func (u *MysqlService) UpFile(mysqlName string, files []*multipart.FileHeader) e
|
|||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, _ = io.Copy(out, src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) RecoverByUpload(req dto.UploadRecover) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(req.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localDir, err := loadLocalDir()
|
||||
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/database/%s/%s/upload/tmp/%s", localDir, app.Key, req.MysqlName, 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
|
||||
}
|
||||
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)
|
||||
}()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -175,7 +246,11 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
|
|||
if mysqlDto.Username == "root" {
|
||||
return errors.New("Cannot set root as user name")
|
||||
}
|
||||
mysql, _ := mysqlRepo.Get(commonRepo.WithByName(mysqlDto.Name))
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(mysqlDto.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, _ := mysqlRepo.Get(commonRepo.WithByName(mysqlDto.Name), mysqlRepo.WithByMysqlName(app.Key))
|
||||
if mysql.ID != 0 {
|
||||
return constant.ErrRecordExist
|
||||
}
|
||||
|
@ -183,15 +258,11 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
|
|||
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(mysqlDto.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create database if not exists %s character set=%s", mysqlDto.Name, mysqlDto.Format)); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPermission := mysqlDto.Permission
|
||||
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create user if not exists '%s'@'%s' identified by '%s';", mysqlDto.Name, tmpPermission, mysqlDto.Password)); err != nil {
|
||||
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create user if not exists '%s'@'%s' identified by '%s';", mysqlDto.Username, tmpPermission, mysqlDto.Password)); err != nil {
|
||||
return err
|
||||
}
|
||||
grantStr := fmt.Sprintf("grant all privileges on %s.* to '%s'@'%s'", mysqlDto.Name, mysqlDto.Username, tmpPermission)
|
||||
|
@ -208,19 +279,11 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
|
|||
}
|
||||
|
||||
func (u *MysqlService) Backup(db dto.BackupDB) error {
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
localDir, appKey, err := loadBackupDirAndKey(db.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(db.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupDir := fmt.Sprintf("database/%s/%s/%s", app.Key, db.MysqlName, db.DBName)
|
||||
backupDir := fmt.Sprintf("database/%s/%s/%s", appKey, 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
|
||||
|
@ -235,12 +298,12 @@ func (u *MysqlService) Recover(db dto.RecoverDB) error {
|
|||
}
|
||||
gzipFile, err := os.Open(db.BackupName)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
defer gzipFile.Close()
|
||||
gzipReader, err := gzip.NewReader(gzipFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, db.DBName)
|
||||
|
@ -356,6 +419,26 @@ func (u *MysqlService) ChangeInfo(info dto.ChangeDBInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) UpdateConfByFile(info dto.MysqlConfUpdateByFile) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(info.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", constant.AppInstallDir, app.Key, app.Name)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(info.File)
|
||||
write.Flush()
|
||||
if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, app.Key, app.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) UpdateVariables(mysqlName string, updatas []dto.MysqlVariablesUpdate) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(mysqlName)
|
||||
if err != nil {
|
||||
|
@ -568,12 +651,16 @@ func backupMysql(backupType, baseDir, backupDir, mysqlName, dbName, fileName str
|
|||
|
||||
func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string {
|
||||
isOn := false
|
||||
hasGroup := false
|
||||
hasKey := false
|
||||
regItem, _ := regexp.Compile(`\[*\]`)
|
||||
var newFiles []string
|
||||
i := 0
|
||||
for _, line := range oldFiles {
|
||||
i++
|
||||
if strings.HasPrefix(line, group) {
|
||||
isOn = true
|
||||
hasGroup = true
|
||||
newFiles = append(newFiles, line)
|
||||
continue
|
||||
}
|
||||
|
@ -586,19 +673,31 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
|
|||
hasKey = true
|
||||
continue
|
||||
}
|
||||
isDeadLine := regItem.Match([]byte(line))
|
||||
if !isDeadLine {
|
||||
if regItem.Match([]byte(line)) || i == len(oldFiles) {
|
||||
isOn = false
|
||||
if !hasKey {
|
||||
newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
|
||||
}
|
||||
newFiles = append(newFiles, line)
|
||||
continue
|
||||
}
|
||||
if !hasKey {
|
||||
newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
|
||||
newFiles = append(newFiles, line)
|
||||
}
|
||||
newFiles = append(newFiles, line)
|
||||
}
|
||||
if !isOn {
|
||||
if !hasGroup {
|
||||
newFiles = append(newFiles, group+"\n")
|
||||
newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
|
||||
}
|
||||
return newFiles
|
||||
}
|
||||
|
||||
func loadBackupDirAndKey(mysqlName string) (string, string, error) {
|
||||
app, err := mysqlRepo.LoadBaseInfoByName(mysqlName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
localDir, err := loadLocalDir()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return localDir, app.Key, nil
|
||||
}
|
||||
|
|
|
@ -178,11 +178,7 @@ func (u *RedisService) Backup() error {
|
|||
if stdout, err := cmd.CombinedOutput(); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
localDir, err := loadLocalDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -255,25 +251,24 @@ func (u *RedisService) Recover(req dto.RedisBackupRecover) error {
|
|||
|
||||
func (u *RedisService) SearchBackupListWithPage(req dto.PageInfo) (int64, interface{}, error) {
|
||||
var (
|
||||
list []dto.RedisBackupRecords
|
||||
backDatas []dto.RedisBackupRecords
|
||||
list []dto.DatabaseFileRecords
|
||||
backDatas []dto.DatabaseFileRecords
|
||||
)
|
||||
redisInfo, err := mysqlRepo.LoadRedisBaseInfo()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL"))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
localDir, err := loadLocalDir(backupLocal)
|
||||
localDir, err := loadLocalDir()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
backupDir := fmt.Sprintf("%s/database/redis/%s", localDir, redisInfo.Name)
|
||||
_ = filepath.Walk(backupDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
list = append(list, dto.RedisBackupRecords{
|
||||
list = append(list, dto.DatabaseFileRecords{
|
||||
CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Size: int(info.Size()),
|
||||
FileDir: backupDir,
|
||||
|
@ -284,7 +279,7 @@ func (u *RedisService) SearchBackupListWithPage(req dto.PageInfo) (int64, interf
|
|||
})
|
||||
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
backDatas = make([]dto.RedisBackupRecords, 0)
|
||||
backDatas = make([]dto.DatabaseFileRecords, 0)
|
||||
} else {
|
||||
if end >= total {
|
||||
end = total
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMysql(t *testing.T) {
|
||||
path := "/Users/slooop/go/src/github.com/1Panel/apps/mysql/5.7.39/conf/my.cnf"
|
||||
|
||||
var lines []string
|
||||
lineBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
lines = strings.Split(string(lineBytes), "\n")
|
||||
}
|
||||
var newLines []string
|
||||
|
||||
start := "[mysqld]"
|
||||
isOn := false
|
||||
hasKey := false
|
||||
regItem, _ := regexp.Compile(`^\[*\]`)
|
||||
i := 0
|
||||
for _, line := range lines {
|
||||
i++
|
||||
if strings.HasPrefix(line, start) {
|
||||
isOn = true
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
if !isOn {
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "user") || strings.HasPrefix(line, "# user") {
|
||||
newLines = append(newLines, "user="+"ON")
|
||||
hasKey = true
|
||||
continue
|
||||
}
|
||||
isDeadLine := regItem.Match([]byte(line))
|
||||
if !isDeadLine {
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
if !hasKey {
|
||||
newLines = append(newLines, "user="+"ON \n")
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.WriteString(strings.Join(newLines, "\n"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
|
@ -2,12 +2,16 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type dbStr string
|
||||
|
||||
func getTxAndContext() (tx *gorm.DB, ctx context.Context) {
|
||||
db := dbStr("db")
|
||||
tx = global.DB.Begin()
|
||||
ctx = context.WithValue(context.Background(), "db", tx)
|
||||
ctx = context.WithValue(context.Background(), db, tx)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
|
|||
withRecordRouter.POST("/backup", baseApi.BackupMysql)
|
||||
withRecordRouter.POST("/uplist", baseApi.MysqlUpList)
|
||||
withRecordRouter.POST("/uplist/upload/:mysqlName", baseApi.UploadMysqlFiles)
|
||||
withRecordRouter.POST("/recover/byupload", baseApi.RecoverMysqlByUpload)
|
||||
withRecordRouter.POST("/recover", baseApi.RecoverMysql)
|
||||
withRecordRouter.POST("/backups/search", baseApi.SearchDBBackups)
|
||||
withRecordRouter.POST("/del", baseApi.DeleteMysql)
|
||||
|
|
|
@ -17,6 +17,12 @@ export namespace Database {
|
|||
dbName: string;
|
||||
backupName: string;
|
||||
}
|
||||
export interface RecoverByUpload {
|
||||
mysqlName: string;
|
||||
dbName: string;
|
||||
fileName: string;
|
||||
fileDir: string;
|
||||
}
|
||||
export interface MysqlDBInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
|
@ -167,7 +173,7 @@ export namespace Database {
|
|||
createdAt: string;
|
||||
size: string;
|
||||
}
|
||||
export interface RedisBackupDelete {
|
||||
export interface FileRecordDelete {
|
||||
fileDir: string;
|
||||
names: Array<string>;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ export const backup = (params: Database.Backup) => {
|
|||
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 searchBackupRecords = (params: Database.SearchBackupRecord) => {
|
||||
return http.post<ResPage<Backup.RecordInfo>>(`/databases/backups/search`, params);
|
||||
};
|
||||
|
@ -84,6 +87,6 @@ export const recoverRedis = (param: Database.RedisRecover) => {
|
|||
export const redisBackupRedisRecords = (param: ReqPage) => {
|
||||
return http.post<ResPage<Database.FileRecord>>(`/databases/redis/backup/records`, param);
|
||||
};
|
||||
export const deleteBackupRedis = (param: Database.RedisBackupDelete) => {
|
||||
export const deleteDatabaseFile = (param: Database.FileRecordDelete) => {
|
||||
return http.post(`/databases/redis/backup/del`, param);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,10 @@ export default {
|
|||
handle: 'Handle',
|
||||
expand: 'Expand',
|
||||
log: 'Log',
|
||||
back: 'Back',
|
||||
recover: 'Recover',
|
||||
upload: 'Upload',
|
||||
download: 'Download',
|
||||
saveAndEnable: 'Save and enable',
|
||||
},
|
||||
search: {
|
||||
|
@ -174,6 +178,13 @@ export default {
|
|||
portHelper:
|
||||
'This port is the exposed port of the container. You need to save the modification separately and restart the container!',
|
||||
|
||||
unSupportType: 'Current file type is not supported!',
|
||||
unSupportSize: 'The uploaded file exceeds 10M, please confirm!',
|
||||
selectFile: 'Select file',
|
||||
supportUpType: 'Only sql, zip, sql.gz, and (tar.gz gz tgz) files within 10 MB are supported',
|
||||
zipFormat:
|
||||
'zip, tar.gz compressed package structure: test.zip or test.tar.gz compressed package must contain test.sql',
|
||||
|
||||
currentStatus: 'Current state',
|
||||
runTime: 'Startup time',
|
||||
connections: 'Total connections',
|
||||
|
|
|
@ -28,6 +28,7 @@ export default {
|
|||
log: '日志',
|
||||
back: '返回',
|
||||
recover: '恢复',
|
||||
upload: '上传',
|
||||
download: '下载',
|
||||
saveAndEnable: '保存并启用',
|
||||
},
|
||||
|
@ -174,7 +175,11 @@ export default {
|
|||
confChange: '配置修改',
|
||||
portHelper: '该端口为容器对外暴露端口,修改需要单独保存并且重启容器!',
|
||||
|
||||
unSupportType: '不支持当前文件类型',
|
||||
unSupportType: '不支持当前文件类型!',
|
||||
unSupportSize: '上传文件超过 10M,请确认!',
|
||||
selectFile: '选择文件',
|
||||
supportUpType: '仅支持 10M 以内 sql、zip、sql.gz、(tar.gz gz tgz) 文件',
|
||||
zipFormat: 'zip、tar.gz 压缩包结构:test.zip 或 test.tar.gz 压缩包内,必需包含 test.sql',
|
||||
|
||||
currentStatus: '当前状态',
|
||||
runTime: '启动时间',
|
||||
|
|
|
@ -91,7 +91,7 @@ const onRecover = async (row: Backup.RecordInfo) => {
|
|||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName.value,
|
||||
backupName: row.fileDir + row.fileName,
|
||||
backupName: row.fileDir + '/' + row.fileName,
|
||||
};
|
||||
await recover(params);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
|
||||
<template #toolbar>
|
||||
<el-button type="primary" @click="onOpenDialog()">{{ $t('commons.button.create') }}</el-button>
|
||||
<el-button @click="onOpenDialog()">phpMyAdmin</el-button>
|
||||
<el-button>phpMyAdmin</el-button>
|
||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
|
@ -44,7 +44,26 @@
|
|||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" />
|
||||
<el-table-column :label="$t('auth.username')" prop="username" />
|
||||
<el-table-column :label="$t('auth.password')" prop="password" />
|
||||
<el-table-column :label="$t('auth.password')" prop="password">
|
||||
<template #default="{ row }">
|
||||
<div v-if="!row.showPassword">
|
||||
<span style="float: left">***********</span>
|
||||
<div style="margin-top: 2px; cursor: pointer">
|
||||
<el-icon style="margin-left: 5px" @click="row.showPassword = true" :size="16">
|
||||
<View />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span style="float: left">{{ row.password }}</span>
|
||||
<div style="margin-top: 4px; cursor: pointer">
|
||||
<el-icon style="margin-left: 5px" @click="row.showPassword = false" :size="16">
|
||||
<Hide />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.description')" prop="description" />
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
|
@ -274,9 +293,10 @@ const buttons = [
|
|||
},
|
||||
{
|
||||
label: i18n.global.t('database.loadBackup'),
|
||||
click: () => {
|
||||
click: (row: Database.MysqlDBInfo) => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: row.name,
|
||||
};
|
||||
uploadRef.value!.acceptParams(params);
|
||||
},
|
||||
|
|
|
@ -71,7 +71,7 @@ const acceptParams = (params: DialogProps): void => {
|
|||
variables.long_query_time = Number(params.variables.long_query_time);
|
||||
|
||||
if (variables.slow_query_log === 'ON') {
|
||||
let path = `/opt/1Panel/data/apps/${mysqlKey.value}/${mysqlName.value}/data/onepanel-slow.log`;
|
||||
let path = `/opt/1Panel/data/apps/${mysqlKey.value}/${mysqlName.value}/data/1Panel-slow.log`;
|
||||
loadMysqlSlowlogs(path);
|
||||
}
|
||||
oldVariables.value = { ...variables };
|
||||
|
@ -91,10 +91,10 @@ const onSave = async () => {
|
|||
if (variables.slow_query_log !== oldVariables.value.slow_query_log) {
|
||||
param.push({ param: 'slow_query_log', value: variables.slow_query_log });
|
||||
}
|
||||
if (variables.long_query_time !== oldVariables.value.long_query_time) {
|
||||
if (variables.slow_query_log === 'ON') {
|
||||
param.push({ param: 'long_query_time', value: variables.long_query_time });
|
||||
param.push({ param: 'slow_query_log_file', value: '/var/lib/mysql/1Panel-slow.log' });
|
||||
}
|
||||
param.push({ param: 'slow_query_log_file', value: '/var/lib/mysql/onepanel-slow.log' });
|
||||
await updateMysqlVariables(mysqlName.value, param);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
|
|
@ -14,13 +14,17 @@
|
|||
:auto-upload="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button>选择文件</el-button>
|
||||
<el-button type="primary" plain>{{ $t('database.selectFile') }}</el-button>
|
||||
</template>
|
||||
<el-button style="margin-left: 10px" @click="onSubmit">上传</el-button>
|
||||
<el-button style="margin-left: 10px" icon="Upload" @click="onSubmit">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div style="margin-left: 10px">
|
||||
<span class="input-help">仅支持sql、zip、sql.gz、(tar.gz|gz|tgz)</span>
|
||||
<span class="input-help">zip、tar.gz压缩包结构:test.zip或test.tar.gz压缩包内,必需包含test.sql</span>
|
||||
<span class="input-help">{{ $t('database.supportUpType') }}</span>
|
||||
<span class="input-help">
|
||||
{{ $t('database.zipFormat') }}
|
||||
</span>
|
||||
</div>
|
||||
<el-divider />
|
||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data">
|
||||
|
@ -61,11 +65,10 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
|||
import { reactive, ref } from 'vue';
|
||||
import { computeSize } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { recover, searchUpList, uploadFile } from '@/api/modules/database';
|
||||
import { deleteDatabaseFile, recoverByUpload, searchUpList, uploadFile } from '@/api/modules/database';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessage, UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus';
|
||||
import { deleteBackupRecord } from '@/api/modules/backup';
|
||||
import { Backup } from '@/api/interface/backup';
|
||||
import { Database } from '@/api/interface/database';
|
||||
|
||||
const selects = ref<any>([]);
|
||||
|
||||
|
@ -101,13 +104,14 @@ const search = async () => {
|
|||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const onRecover = async (row: Backup.RecordInfo) => {
|
||||
const onRecover = async (row: Database.FileRecord) => {
|
||||
let params = {
|
||||
mysqlName: mysqlName.value,
|
||||
dbName: dbName.value,
|
||||
backupName: row.fileDir + row.fileName,
|
||||
fileDir: row.fileDir,
|
||||
fileName: row.fileName,
|
||||
};
|
||||
await recover(params);
|
||||
await recoverByUpload(params);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
|
@ -115,9 +119,17 @@ const uploaderFiles = ref<UploadFiles>([]);
|
|||
const uploadRef = ref<UploadInstance>();
|
||||
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
if (rawFile.name.endsWith('.sql') || rawFile.name.endsWith('gz') || rawFile.name.endsWith('.zip')) {
|
||||
if (
|
||||
rawFile.name.endsWith('.sql') ||
|
||||
rawFile.name.endsWith('.gz') ||
|
||||
rawFile.name.endsWith('.zip') ||
|
||||
rawFile.name.endsWith('.tgz')
|
||||
) {
|
||||
ElMessage.error(i18n.global.t('database.unSupportType'));
|
||||
return false;
|
||||
} else if (rawFile.size / 1024 / 1024 > 10) {
|
||||
ElMessage.error(i18n.global.t('database.unSupportSize'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@ -145,29 +157,32 @@ const onSubmit = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const onBatchDelete = async (row: Backup.RecordInfo | null) => {
|
||||
let ids: Array<number> = [];
|
||||
const onBatchDelete = async (row: Database.FileRecord | null) => {
|
||||
let names: Array<string> = [];
|
||||
let fileDir: string = '';
|
||||
if (row) {
|
||||
ids.push(row.id);
|
||||
fileDir = row.fileDir;
|
||||
names.push(row.fileName);
|
||||
} else {
|
||||
selects.value.forEach((item: Backup.RecordInfo) => {
|
||||
ids.push(item.id);
|
||||
selects.value.forEach((item: Database.FileRecord) => {
|
||||
fileDir = item.fileDir;
|
||||
names.push(item.fileName);
|
||||
});
|
||||
}
|
||||
await useDeleteData(deleteBackupRecord, { ids: ids }, 'commons.msg.delete', true);
|
||||
await useDeleteData(deleteDatabaseFile, { fileDir: fileDir, names: names }, 'commons.msg.delete', true);
|
||||
search();
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
click: (row: Database.FileRecord) => {
|
||||
onRecover(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
click: (row: Database.FileRecord) => {
|
||||
onBatchDelete(row);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -124,7 +124,7 @@ import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
|||
import { Database } from '@/api/interface/database';
|
||||
import {
|
||||
backupRedis,
|
||||
deleteBackupRedis,
|
||||
deleteDatabaseFile,
|
||||
recoverRedis,
|
||||
redisBackupRedisRecords,
|
||||
RedisPersistenceConf,
|
||||
|
@ -166,7 +166,6 @@ const data = ref();
|
|||
const selects = ref<any>([]);
|
||||
const currentRow = ref();
|
||||
const confirmDialogRef = ref();
|
||||
const submitInput = ref();
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
|
@ -199,35 +198,33 @@ const onBackup = async () => {
|
|||
loadBackupRecords();
|
||||
};
|
||||
const onRecover = async () => {
|
||||
if (submitInput.value === i18n.global.t('database.submitIt')) {
|
||||
let param = {
|
||||
fileName: currentRow.value.fileName,
|
||||
fileDir: currentRow.value.fileDir,
|
||||
};
|
||||
await recoverRedis(param);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
}
|
||||
let param = {
|
||||
fileName: currentRow.value.fileName,
|
||||
fileDir: currentRow.value.fileDir,
|
||||
};
|
||||
await recoverRedis(param);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
const onBatchDelete = async (row: Database.RedisBackupRecord | null) => {
|
||||
const onBatchDelete = async (row: Database.FileRecord | null) => {
|
||||
let names: Array<string> = [];
|
||||
let fileDir: string = '';
|
||||
if (row) {
|
||||
fileDir = row.fileDir;
|
||||
names.push(row.fileName);
|
||||
} else {
|
||||
selects.value.forEach((item: Database.RedisBackupRecord) => {
|
||||
selects.value.forEach((item: Database.FileRecord) => {
|
||||
fileDir = item.fileDir;
|
||||
names.push(item.fileName);
|
||||
});
|
||||
}
|
||||
await useDeleteData(deleteBackupRedis, { fileDir: fileDir, names: names }, 'commons.msg.delete', true);
|
||||
await useDeleteData(deleteDatabaseFile, { fileDir: fileDir, names: names }, 'commons.msg.delete', true);
|
||||
loadBackupRecords();
|
||||
};
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.recover'),
|
||||
click: (row: Database.RedisBackupRecord) => {
|
||||
click: (row: Database.FileRecord) => {
|
||||
currentRow.value = row;
|
||||
let params = {
|
||||
header: i18n.global.t('commons.button.recover'),
|
||||
|
@ -239,7 +236,7 @@ const buttons = [
|
|||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Database.RedisBackupRecord) => {
|
||||
click: (row: Database.FileRecord) => {
|
||||
onBatchDelete(row);
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue