1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-14 01:34:47 +08:00

feat: 完成 mysql 从上传恢复功能

This commit is contained in:
ssongliu 2022-11-09 15:08:38 +08:00 committed by ssongliu
parent c49d2ef243
commit 581c940336
18 changed files with 295 additions and 196 deletions

View File

@ -1,10 +1,7 @@
package v1 package v1
import ( import (
"bufio"
"errors" "errors"
"fmt"
"os"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
@ -118,21 +115,10 @@ func (b *BaseApi) UpdateMysqlConfByFile(c *gin.Context) {
return return
} }
mysqlInfo, err := mysqlService.LoadBaseInfo(req.MysqlName) if err := mysqlService.UpdateConfByFile(req); err != nil {
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return 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) helper.SuccessWithData(c, nil)
} }
@ -206,6 +192,21 @@ func (b *BaseApi) BackupMysql(c *gin.Context) {
helper.SuccessWithData(c, nil) 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) { func (b *BaseApi) RecoverMysql(c *gin.Context) {
var req dto.RecoverDB var req dto.RecoverDB
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -131,6 +131,13 @@ type RecoverDB struct {
BackupName string `json:"backupName" validate:"required"` 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 // redis
type RedisConfUpdate struct { type RedisConfUpdate struct {
Timeout string `json:"timeout"` Timeout string `json:"timeout"`
@ -181,7 +188,7 @@ type RedisStatus struct {
LatestForkUsec string `json:"latest_fork_usec"` LatestForkUsec string `json:"latest_fork_usec"`
} }
type RedisBackupRecords struct { type DatabaseFileRecords struct {
FileName string `json:"fileName"` FileName string `json:"fileName"`
FileDir string `json:"fileDir"` FileDir string `json:"fileDir"`
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`

View File

@ -201,7 +201,11 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl
return backClient, nil 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{}) varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
return "", err return "", err

View File

@ -83,11 +83,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
return "", err return "", err
} }
if cronjob.KeepLocal || cronjob.Type != "LOCAL" { if cronjob.KeepLocal || cronjob.Type != "LOCAL" {
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) localDir, err := loadLocalDir()
if err != nil {
return "", err
}
localDir, err := loadLocalDir(backupLocal)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,9 +1,11 @@
package service package service
import ( import (
"bufio"
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"os" "os"
@ -19,6 +21,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/compose" "github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/files"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -33,8 +36,10 @@ type IMysqlService interface {
Create(mysqlDto dto.MysqlDBCreate) error Create(mysqlDto dto.MysqlDBCreate) error
ChangeInfo(info dto.ChangeDBInfo) error ChangeInfo(info dto.ChangeDBInfo) error
UpdateVariables(mysqlName string, updatas []dto.MysqlVariablesUpdate) error UpdateVariables(mysqlName string, updatas []dto.MysqlVariablesUpdate) error
UpdateConfByFile(info dto.MysqlConfUpdateByFile) error
UpFile(mysqlName string, files []*multipart.FileHeader) error UpFile(mysqlName string, files []*multipart.FileHeader) error
RecoverByUpload(req dto.UploadRecover) error
SearchUpListWithPage(req dto.SearchDBWithPage) (int64, interface{}, error) SearchUpListWithPage(req dto.SearchDBWithPage) (int64, interface{}, error)
Backup(db dto.BackupDB) error Backup(db dto.BackupDB) error
Recover(db dto.RecoverDB) 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) { func (u *MysqlService) SearchUpListWithPage(req dto.SearchDBWithPage) (int64, interface{}, error) {
var ( var (
list []dto.RedisBackupRecords list []dto.DatabaseFileRecords
backDatas []dto.RedisBackupRecords backDatas []dto.DatabaseFileRecords
) )
redisInfo, err := mysqlRepo.LoadBaseInfoByName(req.MysqlName) localDir, appKey, err := loadBackupDirAndKey(req.MysqlName)
if err != nil { if err != nil {
return 0, nil, err return 0, list, nil
} }
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) uploadDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, appKey, req.MysqlName)
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)
if _, err := os.Stat(uploadDir); err != nil { if _, err := os.Stat(uploadDir); err != nil {
return 0, list, nil return 0, list, nil
} }
_ = filepath.Walk(uploadDir, func(path string, info os.FileInfo, err error) error { _ = filepath.Walk(uploadDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() { if !info.IsDir() {
list = append(list, dto.RedisBackupRecords{ list = append(list, dto.DatabaseFileRecords{
CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"), CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"),
Size: int(info.Size()), Size: int(info.Size()),
FileDir: uploadDir, 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 total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total { if start > total {
backDatas = make([]dto.RedisBackupRecords, 0) backDatas = make([]dto.DatabaseFileRecords, 0)
} else { } else {
if end >= total { if end >= total {
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 { 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 { if err != nil {
return err return err
} }
app, err := mysqlRepo.LoadBaseInfoByName(mysqlName) dstDir := fmt.Sprintf("%s/database/%s/%s/upload", localDir, appKey, 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)
if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { if err = os.MkdirAll(dstDir, os.ModePerm); err != nil {
if err != nil { if err != nil {
@ -139,6 +131,85 @@ func (u *MysqlService) UpFile(mysqlName string, files []*multipart.FileHeader) e
return err return err
} }
defer out.Close() 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 return nil
} }
@ -175,7 +246,11 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
if mysqlDto.Username == "root" { if mysqlDto.Username == "root" {
return errors.New("Cannot set root as user name") 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 { if mysql.ID != 0 {
return constant.ErrRecordExist return constant.ErrRecordExist
} }
@ -183,15 +258,11 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
return errors.WithMessage(constant.ErrStructTransform, err.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 { 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 return err
} }
tmpPermission := mysqlDto.Permission 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 return err
} }
grantStr := fmt.Sprintf("grant all privileges on %s.* to '%s'@'%s'", mysqlDto.Name, mysqlDto.Username, tmpPermission) 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 { func (u *MysqlService) Backup(db dto.BackupDB) error {
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) localDir, appKey, err := loadBackupDirAndKey(db.MysqlName)
if err != nil { if err != nil {
return err return err
} }
app, err := mysqlRepo.LoadBaseInfoByName(db.MysqlName) backupDir := fmt.Sprintf("database/%s/%s/%s", appKey, db.MysqlName, db.DBName)
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)
fileName := fmt.Sprintf("%s_%s.sql.gz", db.DBName, time.Now().Format("20060102150405")) 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 { if err := backupMysql("LOCAL", localDir, backupDir, db.MysqlName, db.DBName, fileName); err != nil {
return err return err
@ -235,12 +298,12 @@ func (u *MysqlService) Recover(db dto.RecoverDB) error {
} }
gzipFile, err := os.Open(db.BackupName) gzipFile, err := os.Open(db.BackupName)
if err != nil { if err != nil {
fmt.Println(err) return err
} }
defer gzipFile.Close() defer gzipFile.Close()
gzipReader, err := gzip.NewReader(gzipFile) gzipReader, err := gzip.NewReader(gzipFile)
if err != nil { if err != nil {
fmt.Println(err) return err
} }
defer gzipReader.Close() defer gzipReader.Close()
cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, db.DBName) 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 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 { func (u *MysqlService) UpdateVariables(mysqlName string, updatas []dto.MysqlVariablesUpdate) error {
app, err := mysqlRepo.LoadBaseInfoByName(mysqlName) app, err := mysqlRepo.LoadBaseInfoByName(mysqlName)
if err != nil { 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 { func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string {
isOn := false isOn := false
hasGroup := false
hasKey := false hasKey := false
regItem, _ := regexp.Compile(`\[*\]`) regItem, _ := regexp.Compile(`\[*\]`)
var newFiles []string var newFiles []string
i := 0
for _, line := range oldFiles { for _, line := range oldFiles {
i++
if strings.HasPrefix(line, group) { if strings.HasPrefix(line, group) {
isOn = true isOn = true
hasGroup = true
newFiles = append(newFiles, line) newFiles = append(newFiles, line)
continue continue
} }
@ -586,19 +673,31 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
hasKey = true hasKey = true
continue continue
} }
isDeadLine := regItem.Match([]byte(line)) if regItem.Match([]byte(line)) || i == len(oldFiles) {
if !isDeadLine { isOn = false
if !hasKey {
newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
}
newFiles = append(newFiles, line) newFiles = append(newFiles, line)
continue continue
} }
if !hasKey { newFiles = append(newFiles, line)
newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
newFiles = append(newFiles, line)
}
} }
if !isOn { if !hasGroup {
newFiles = append(newFiles, group+"\n") newFiles = append(newFiles, group+"\n")
newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value)) newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
} }
return newFiles 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
}

View File

@ -178,11 +178,7 @@ func (u *RedisService) Backup() error {
if stdout, err := cmd.CombinedOutput(); err != nil { if stdout, err := cmd.CombinedOutput(); err != nil {
return errors.New(string(stdout)) return errors.New(string(stdout))
} }
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) localDir, err := loadLocalDir()
if err != nil {
return err
}
localDir, err := loadLocalDir(backupLocal)
if err != nil { if err != nil {
return err return err
} }
@ -255,25 +251,24 @@ func (u *RedisService) Recover(req dto.RedisBackupRecover) error {
func (u *RedisService) SearchBackupListWithPage(req dto.PageInfo) (int64, interface{}, error) { func (u *RedisService) SearchBackupListWithPage(req dto.PageInfo) (int64, interface{}, error) {
var ( var (
list []dto.RedisBackupRecords list []dto.DatabaseFileRecords
backDatas []dto.RedisBackupRecords backDatas []dto.DatabaseFileRecords
) )
redisInfo, err := mysqlRepo.LoadRedisBaseInfo() redisInfo, err := mysqlRepo.LoadRedisBaseInfo()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
backupLocal, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) localDir, err := loadLocalDir()
if err != nil {
return 0, nil, err
}
localDir, err := loadLocalDir(backupLocal)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
backupDir := fmt.Sprintf("%s/database/redis/%s", localDir, redisInfo.Name) backupDir := fmt.Sprintf("%s/database/redis/%s", localDir, redisInfo.Name)
_ = filepath.Walk(backupDir, func(path string, info os.FileInfo, err error) error { _ = filepath.Walk(backupDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() { if !info.IsDir() {
list = append(list, dto.RedisBackupRecords{ list = append(list, dto.DatabaseFileRecords{
CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"), CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"),
Size: int(info.Size()), Size: int(info.Size()),
FileDir: backupDir, 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 total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total { if start > total {
backDatas = make([]dto.RedisBackupRecords, 0) backDatas = make([]dto.DatabaseFileRecords, 0)
} else { } else {
if end >= total { if end >= total {
end = total end = total

View File

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

View File

@ -2,12 +2,16 @@ package service
import ( import (
"context" "context"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm" "gorm.io/gorm"
) )
type dbStr string
func getTxAndContext() (tx *gorm.DB, ctx context.Context) { func getTxAndContext() (tx *gorm.DB, ctx context.Context) {
db := dbStr("db")
tx = global.DB.Begin() tx = global.DB.Begin()
ctx = context.WithValue(context.Background(), "db", tx) ctx = context.WithValue(context.Background(), db, tx)
return return
} }

View File

@ -26,6 +26,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
withRecordRouter.POST("/backup", baseApi.BackupMysql) withRecordRouter.POST("/backup", baseApi.BackupMysql)
withRecordRouter.POST("/uplist", baseApi.MysqlUpList) withRecordRouter.POST("/uplist", baseApi.MysqlUpList)
withRecordRouter.POST("/uplist/upload/:mysqlName", baseApi.UploadMysqlFiles) withRecordRouter.POST("/uplist/upload/:mysqlName", baseApi.UploadMysqlFiles)
withRecordRouter.POST("/recover/byupload", baseApi.RecoverMysqlByUpload)
withRecordRouter.POST("/recover", baseApi.RecoverMysql) withRecordRouter.POST("/recover", baseApi.RecoverMysql)
withRecordRouter.POST("/backups/search", baseApi.SearchDBBackups) withRecordRouter.POST("/backups/search", baseApi.SearchDBBackups)
withRecordRouter.POST("/del", baseApi.DeleteMysql) withRecordRouter.POST("/del", baseApi.DeleteMysql)

View File

@ -17,6 +17,12 @@ export namespace Database {
dbName: string; dbName: string;
backupName: string; backupName: string;
} }
export interface RecoverByUpload {
mysqlName: string;
dbName: string;
fileName: string;
fileDir: string;
}
export interface MysqlDBInfo { export interface MysqlDBInfo {
id: number; id: number;
createdAt: Date; createdAt: Date;
@ -167,7 +173,7 @@ export namespace Database {
createdAt: string; createdAt: string;
size: string; size: string;
} }
export interface RedisBackupDelete { export interface FileRecordDelete {
fileDir: string; fileDir: string;
names: Array<string>; names: Array<string>;
} }

View File

@ -23,6 +23,9 @@ export const backup = (params: Database.Backup) => {
export const recover = (params: Database.Recover) => { export const recover = (params: Database.Recover) => {
return http.post(`/databases/recover`, params); 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) => { export const searchBackupRecords = (params: Database.SearchBackupRecord) => {
return http.post<ResPage<Backup.RecordInfo>>(`/databases/backups/search`, params); 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) => { export const redisBackupRedisRecords = (param: ReqPage) => {
return http.post<ResPage<Database.FileRecord>>(`/databases/redis/backup/records`, param); 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); return http.post(`/databases/redis/backup/del`, param);
}; };

View File

@ -25,6 +25,10 @@ export default {
handle: 'Handle', handle: 'Handle',
expand: 'Expand', expand: 'Expand',
log: 'Log', log: 'Log',
back: 'Back',
recover: 'Recover',
upload: 'Upload',
download: 'Download',
saveAndEnable: 'Save and enable', saveAndEnable: 'Save and enable',
}, },
search: { search: {
@ -174,6 +178,13 @@ export default {
portHelper: portHelper:
'This port is the exposed port of the container. You need to save the modification separately and restart the container!', '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', currentStatus: 'Current state',
runTime: 'Startup time', runTime: 'Startup time',
connections: 'Total connections', connections: 'Total connections',

View File

@ -28,6 +28,7 @@ export default {
log: '日志', log: '日志',
back: '返回', back: '返回',
recover: '恢复', recover: '恢复',
upload: '上传',
download: '下载', download: '下载',
saveAndEnable: '保存并启用', saveAndEnable: '保存并启用',
}, },
@ -174,7 +175,11 @@ export default {
confChange: '配置修改', confChange: '配置修改',
portHelper: '该端口为容器对外暴露端口修改需要单独保存并且重启容器', portHelper: '该端口为容器对外暴露端口修改需要单独保存并且重启容器',
unSupportType: '不支持当前文件类型', unSupportType: '不支持当前文件类型',
unSupportSize: '上传文件超过 10M请确认',
selectFile: '选择文件',
supportUpType: '仅支持 10M 以内 sqlzipsql.gz(tar.gz gz tgz) 文件',
zipFormat: 'ziptar.gz 压缩包结构test.zip test.tar.gz 压缩包内必需包含 test.sql',
currentStatus: '当前状态', currentStatus: '当前状态',
runTime: '启动时间', runTime: '启动时间',

View File

@ -91,7 +91,7 @@ const onRecover = async (row: Backup.RecordInfo) => {
let params = { let params = {
mysqlName: mysqlName.value, mysqlName: mysqlName.value,
dbName: dbName.value, dbName: dbName.value,
backupName: row.fileDir + row.fileName, backupName: row.fileDir + '/' + row.fileName,
}; };
await recover(params); await recover(params);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));

View File

@ -36,7 +36,7 @@
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data"> <ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
<template #toolbar> <template #toolbar>
<el-button type="primary" @click="onOpenDialog()">{{ $t('commons.button.create') }}</el-button> <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)"> <el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
{{ $t('commons.button.delete') }} {{ $t('commons.button.delete') }}
</el-button> </el-button>
@ -44,7 +44,26 @@
<el-table-column type="selection" fix /> <el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" prop="name" /> <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.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 :label="$t('commons.table.description')" prop="description" />
<el-table-column <el-table-column
prop="createdAt" prop="createdAt"
@ -274,9 +293,10 @@ const buttons = [
}, },
{ {
label: i18n.global.t('database.loadBackup'), label: i18n.global.t('database.loadBackup'),
click: () => { click: (row: Database.MysqlDBInfo) => {
let params = { let params = {
mysqlName: mysqlName.value, mysqlName: mysqlName.value,
dbName: row.name,
}; };
uploadRef.value!.acceptParams(params); uploadRef.value!.acceptParams(params);
}, },

View File

@ -71,7 +71,7 @@ const acceptParams = (params: DialogProps): void => {
variables.long_query_time = Number(params.variables.long_query_time); variables.long_query_time = Number(params.variables.long_query_time);
if (variables.slow_query_log === 'ON') { 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); loadMysqlSlowlogs(path);
} }
oldVariables.value = { ...variables }; oldVariables.value = { ...variables };
@ -91,10 +91,10 @@ const onSave = async () => {
if (variables.slow_query_log !== oldVariables.value.slow_query_log) { if (variables.slow_query_log !== oldVariables.value.slow_query_log) {
param.push({ param: 'slow_query_log', value: variables.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: '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); await updateMysqlVariables(mysqlName.value, param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}; };

View File

@ -14,13 +14,17 @@
:auto-upload="false" :auto-upload="false"
> >
<template #trigger> <template #trigger>
<el-button>选择文件</el-button> <el-button type="primary" plain>{{ $t('database.selectFile') }}</el-button>
</template> </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> </el-upload>
<div style="margin-left: 10px"> <div style="margin-left: 10px">
<span class="input-help">仅支持sqlzipsql.gz(tar.gz|gz|tgz)</span> <span class="input-help">{{ $t('database.supportUpType') }}</span>
<span class="input-help">ziptar.gz压缩包结构test.zip或test.tar.gz压缩包内必需包含test.sql</span> <span class="input-help">
{{ $t('database.zipFormat') }}
</span>
</div> </div>
<el-divider /> <el-divider />
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data"> <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 { reactive, ref } from 'vue';
import { computeSize } from '@/utils/util'; import { computeSize } from '@/utils/util';
import { useDeleteData } from '@/hooks/use-delete-data'; 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 i18n from '@/lang';
import { ElMessage, UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus'; import { ElMessage, UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus';
import { deleteBackupRecord } from '@/api/modules/backup'; import { Database } from '@/api/interface/database';
import { Backup } from '@/api/interface/backup';
const selects = ref<any>([]); const selects = ref<any>([]);
@ -101,13 +104,14 @@ const search = async () => {
paginationConfig.total = res.data.total; paginationConfig.total = res.data.total;
}; };
const onRecover = async (row: Backup.RecordInfo) => { const onRecover = async (row: Database.FileRecord) => {
let params = { let params = {
mysqlName: mysqlName.value, mysqlName: mysqlName.value,
dbName: dbName.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')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}; };
@ -115,9 +119,17 @@ const uploaderFiles = ref<UploadFiles>([]);
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<UploadInstance>();
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => { 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')); ElMessage.error(i18n.global.t('database.unSupportType'));
return false; return false;
} else if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error(i18n.global.t('database.unSupportSize'));
return false;
} }
return true; return true;
}; };
@ -145,29 +157,32 @@ const onSubmit = () => {
}); });
}; };
const onBatchDelete = async (row: Backup.RecordInfo | null) => { const onBatchDelete = async (row: Database.FileRecord | null) => {
let ids: Array<number> = []; let names: Array<string> = [];
let fileDir: string = '';
if (row) { if (row) {
ids.push(row.id); fileDir = row.fileDir;
names.push(row.fileName);
} else { } else {
selects.value.forEach((item: Backup.RecordInfo) => { selects.value.forEach((item: Database.FileRecord) => {
ids.push(item.id); 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(); search();
}; };
const buttons = [ const buttons = [
{ {
label: i18n.global.t('commons.button.recover'), label: i18n.global.t('commons.button.recover'),
click: (row: Backup.RecordInfo) => { click: (row: Database.FileRecord) => {
onRecover(row); onRecover(row);
}, },
}, },
{ {
label: i18n.global.t('commons.button.delete'), label: i18n.global.t('commons.button.delete'),
click: (row: Backup.RecordInfo) => { click: (row: Database.FileRecord) => {
onBatchDelete(row); onBatchDelete(row);
}, },
}, },

View File

@ -124,7 +124,7 @@ import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { Database } from '@/api/interface/database'; import { Database } from '@/api/interface/database';
import { import {
backupRedis, backupRedis,
deleteBackupRedis, deleteDatabaseFile,
recoverRedis, recoverRedis,
redisBackupRedisRecords, redisBackupRedisRecords,
RedisPersistenceConf, RedisPersistenceConf,
@ -166,7 +166,6 @@ const data = ref();
const selects = ref<any>([]); const selects = ref<any>([]);
const currentRow = ref(); const currentRow = ref();
const confirmDialogRef = ref(); const confirmDialogRef = ref();
const submitInput = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
@ -199,35 +198,33 @@ const onBackup = async () => {
loadBackupRecords(); loadBackupRecords();
}; };
const onRecover = async () => { const onRecover = async () => {
if (submitInput.value === i18n.global.t('database.submitIt')) { let param = {
let param = { fileName: currentRow.value.fileName,
fileName: currentRow.value.fileName, fileDir: currentRow.value.fileDir,
fileDir: currentRow.value.fileDir, };
}; await recoverRedis(param);
await recoverRedis(param); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
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 names: Array<string> = [];
let fileDir: string = ''; let fileDir: string = '';
if (row) { if (row) {
fileDir = row.fileDir; fileDir = row.fileDir;
names.push(row.fileName); names.push(row.fileName);
} else { } else {
selects.value.forEach((item: Database.RedisBackupRecord) => { selects.value.forEach((item: Database.FileRecord) => {
fileDir = item.fileDir; fileDir = item.fileDir;
names.push(item.fileName); 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(); loadBackupRecords();
}; };
const buttons = [ const buttons = [
{ {
label: i18n.global.t('commons.button.recover'), label: i18n.global.t('commons.button.recover'),
click: (row: Database.RedisBackupRecord) => { click: (row: Database.FileRecord) => {
currentRow.value = row; currentRow.value = row;
let params = { let params = {
header: i18n.global.t('commons.button.recover'), header: i18n.global.t('commons.button.recover'),
@ -239,7 +236,7 @@ const buttons = [
}, },
{ {
label: i18n.global.t('commons.button.delete'), label: i18n.global.t('commons.button.delete'),
click: (row: Database.RedisBackupRecord) => { click: (row: Database.FileRecord) => {
onBatchDelete(row); onBatchDelete(row);
}, },
}, },