mirror of
https://gitee.com/fit2cloud-feizhiyun/1Panel.git
synced 2024-11-30 02:47:51 +08:00
feat: 完成 mysql 数据库备份与恢复功能
This commit is contained in:
parent
325bb7bb5f
commit
8cf9c27f5f
@ -61,6 +61,24 @@ func (b *BaseApi) DeleteBackup(c *gin.Context) {
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) DeleteBackupRecord(c *gin.Context) {
|
||||
var req dto.BatchDeleteReq
|
||||
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 := backupService.BatchDeleteRecord(req.Ids); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) UpdateBackup(c *gin.Context) {
|
||||
var req dto.BackupOperate
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
@ -80,6 +80,55 @@ func (b *BaseApi) SearchMysql(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (b *BaseApi) SearchDBBackups(c *gin.Context) {
|
||||
var req dto.SearchBackupsWithPage
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := mysqlService.SearchBacpupsWithPage(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
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 := mysqlService.Backup(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 {
|
||||
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)
|
||||
}
|
||||
|
||||
func (b *BaseApi) DeleteMysql(c *gin.Context) {
|
||||
var req dto.BatchDeleteReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
@ -17,6 +17,21 @@ type BackupInfo struct {
|
||||
Vars string `json:"vars"`
|
||||
}
|
||||
|
||||
type BackupSearch struct {
|
||||
PageInfo
|
||||
Type string `json:"type" validate:"required,oneof=website mysql"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
DetailName string `json:"detailName"`
|
||||
}
|
||||
|
||||
type BackupRecords struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Source string `json:"source"`
|
||||
FileDir string `json:"fileDir"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type ForBuckets struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Credential string `json:"credential" validate:"required"`
|
||||
|
@ -10,6 +10,7 @@ type MysqlDBInfo struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Permission string `json:"permission"`
|
||||
BackupCount int `json:"backupCount"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
@ -115,5 +116,22 @@ type DBBaseInfo struct {
|
||||
|
||||
type SearchDBWithPage struct {
|
||||
PageInfo
|
||||
Version string `json:"version" validate:"required"`
|
||||
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
|
||||
}
|
||||
|
||||
type SearchBackupsWithPage struct {
|
||||
PageInfo
|
||||
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
|
||||
DBName string `json:"dbName" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupDB struct {
|
||||
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
|
||||
DBName string `json:"dbName" validate:"required"`
|
||||
}
|
||||
|
||||
type RecoverDB struct {
|
||||
Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"`
|
||||
DBName string `json:"dbName" validate:"required"`
|
||||
BackupName string `json:"backupName" validate:"required"`
|
||||
}
|
||||
|
@ -7,3 +7,13 @@ type BackupAccount struct {
|
||||
Credential string `gorm:"type:varchar(256)" json:"credential"`
|
||||
Vars string `gorm:"type:longText" json:"vars"`
|
||||
}
|
||||
|
||||
type BackupRecord struct {
|
||||
BaseModel
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
||||
DetailName string `gorm:"type:varchar(256)" json:"detailName"`
|
||||
Source string `gorm:"type:varchar(256)" json:"source"`
|
||||
FileDir string `gorm:"type:varchar(256)" json:"fileDir"`
|
||||
FileName string `gorm:"type:varchar(256)" json:"fileName"`
|
||||
}
|
||||
|
@ -3,16 +3,22 @@ package repo
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BackupRepo struct{}
|
||||
|
||||
type IBackupRepo interface {
|
||||
Get(opts ...DBOption) (model.BackupAccount, error)
|
||||
ListRecord(opts ...DBOption) ([]model.BackupRecord, error)
|
||||
PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error)
|
||||
List(opts ...DBOption) ([]model.BackupAccount, error)
|
||||
Create(backup *model.BackupAccount) error
|
||||
CreateRecord(record *model.BackupRecord) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteRecord(opts ...DBOption) error
|
||||
WithByDetailName(detailName string) DBOption
|
||||
}
|
||||
|
||||
func NewIBackupRepo() IBackupRepo {
|
||||
@ -29,14 +35,43 @@ func (u *BackupRepo) Get(opts ...DBOption) (model.BackupAccount, error) {
|
||||
return backup, err
|
||||
}
|
||||
|
||||
func (u *BackupRepo) ListRecord(opts ...DBOption) ([]model.BackupRecord, error) {
|
||||
var users []model.BackupRecord
|
||||
db := global.DB.Model(&model.BackupRecord{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (u *BackupRepo) PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) {
|
||||
var users []model.BackupRecord
|
||||
db := global.DB.Model(&model.BackupRecord{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
|
||||
return count, users, err
|
||||
}
|
||||
|
||||
func (c *BackupRepo) WithByDetailName(detailName string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(detailName) == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("detail_name = ?", detailName)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *BackupRepo) List(opts ...DBOption) ([]model.BackupAccount, error) {
|
||||
var ops []model.BackupAccount
|
||||
db := global.DB.Model(&model.BackupAccount{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Find(&ops).Error
|
||||
return ops, err
|
||||
}
|
||||
@ -45,6 +80,10 @@ func (u *BackupRepo) Create(backup *model.BackupAccount) error {
|
||||
return global.DB.Create(backup).Error
|
||||
}
|
||||
|
||||
func (u *BackupRepo) CreateRecord(record *model.BackupRecord) error {
|
||||
return global.DB.Create(record).Error
|
||||
}
|
||||
|
||||
func (u *BackupRepo) Update(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.BackupAccount{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
@ -56,3 +95,11 @@ func (u *BackupRepo) Delete(opts ...DBOption) error {
|
||||
}
|
||||
return db.Delete(&model.BackupAccount{}).Error
|
||||
}
|
||||
|
||||
func (u *BackupRepo) DeleteRecord(opts ...DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.BackupRecord{}).Error
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"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/cloud_storage"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@ -15,10 +17,12 @@ type BackupService struct{}
|
||||
|
||||
type IBackupService interface {
|
||||
List() ([]dto.BackupInfo, error)
|
||||
SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error)
|
||||
Create(backupDto dto.BackupOperate) error
|
||||
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
||||
Update(id uint, upMap map[string]interface{}) error
|
||||
BatchDelete(ids []uint) error
|
||||
BatchDeleteRecord(ids []uint) error
|
||||
NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error)
|
||||
}
|
||||
|
||||
@ -39,6 +43,25 @@ func (u *BackupService) List() ([]dto.BackupInfo, error) {
|
||||
return dtobas, err
|
||||
}
|
||||
|
||||
func (u *BackupService) SearchRecordWithPage(search dto.BackupSearch) (int64, []dto.BackupRecords, error) {
|
||||
total, records, err := backupRepo.PageRecord(
|
||||
search.Page, search.PageSize,
|
||||
commonRepo.WithOrderBy("created_at desc"),
|
||||
commonRepo.WithByName(search.Name),
|
||||
commonRepo.WithByType(search.Type),
|
||||
backupRepo.WithByDetailName(search.DetailName),
|
||||
)
|
||||
var dtobas []dto.BackupRecords
|
||||
for _, group := range records {
|
||||
var item dto.BackupRecords
|
||||
if err := copier.Copy(&item, &group); err != nil {
|
||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
dtobas = append(dtobas, item)
|
||||
}
|
||||
return total, dtobas, err
|
||||
}
|
||||
|
||||
func (u *BackupService) Create(backupDto dto.BackupOperate) error {
|
||||
backup, _ := backupRepo.Get(commonRepo.WithByType(backupDto.Type))
|
||||
if backup.ID != 0 {
|
||||
@ -80,6 +103,33 @@ func (u *BackupService) BatchDelete(ids []uint) error {
|
||||
return backupRepo.Delete(commonRepo.WithIdsIn(ids))
|
||||
}
|
||||
|
||||
func (u *BackupService) BatchDeleteRecord(ids []uint) error {
|
||||
records, err := backupRepo.ListRecord(commonRepo.WithIdsIn(ids))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, record := range records {
|
||||
if record.Source == "LOCAL" {
|
||||
if err := os.Remove(record.FileDir + record.FileName); err != nil {
|
||||
global.LOG.Errorf("remove file %s failed, err: %v", record.FileDir+record.FileName, err)
|
||||
}
|
||||
} else {
|
||||
backupAccount, err := backupRepo.Get(commonRepo.WithByName(record.Source))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := u.NewClient(&backupAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = client.Delete(record.FileDir + record.FileName); err != nil {
|
||||
global.LOG.Errorf("remove file %s from %s failed, err: %v", record.FileDir+record.FileName, record.Source, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return backupRepo.DeleteRecord(commonRepo.WithIdsIn(ids))
|
||||
}
|
||||
|
||||
func (u *BackupService) Update(id uint, upMap map[string]interface{}) error {
|
||||
return backupRepo.Update(id, upMap)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -11,6 +13,7 @@ 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/go-sql-driver/mysql"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@ -20,9 +23,14 @@ type MysqlService struct{}
|
||||
|
||||
type IMysqlService interface {
|
||||
SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error)
|
||||
SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error)
|
||||
Create(mysqlDto dto.MysqlDBCreate) error
|
||||
ChangeInfo(info dto.ChangeDBInfo) error
|
||||
UpdateVariables(variables dto.MysqlVariablesUpdate) error
|
||||
|
||||
Backup(db dto.BackupDB) error
|
||||
Recover(db dto.RecoverDB) error
|
||||
|
||||
Delete(version string, ids []uint) error
|
||||
LoadStatus(version string) (*dto.MysqlStatus, error)
|
||||
LoadVariables(version string) (*dto.MysqlVariables, error)
|
||||
@ -47,6 +55,21 @@ func (u *MysqlService) SearchWithPage(search dto.SearchDBWithPage) (int64, inter
|
||||
return total, dtoMysqls, err
|
||||
}
|
||||
|
||||
func (u *MysqlService) SearchBacpupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) {
|
||||
app, err := mysqlRepo.LoadBaseInfoByVersion(search.Version)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
searchDto := dto.BackupSearch{
|
||||
Type: "database-mysql",
|
||||
PageInfo: search.PageInfo,
|
||||
Name: app.Name,
|
||||
DetailName: search.DBName,
|
||||
}
|
||||
|
||||
return NewIBackupService().SearchRecordWithPage(searchDto)
|
||||
}
|
||||
|
||||
func (u *MysqlService) LoadRunningVersion() ([]string, error) {
|
||||
return mysqlRepo.LoadRunningVersion()
|
||||
}
|
||||
@ -87,6 +110,66 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) Backup(db dto.BackupDB) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backupDir := fmt.Sprintf("%s/%s/%s/", constant.DatabaseDir, app.Name, db.DBName)
|
||||
if _, err := os.Stat(backupDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(backupDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
backupName := fmt.Sprintf("%s%s_%s.sql.gz", backupDir, db.DBName, time.Now().Format("20060102150405"))
|
||||
outfile, _ := os.OpenFile(backupName, os.O_RDWR|os.O_CREATE, 0755)
|
||||
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, db.DBName)
|
||||
gzipCmd := exec.Command("gzip", "-cf")
|
||||
gzipCmd.Stdin, _ = cmd.StdoutPipe()
|
||||
gzipCmd.Stdout = outfile
|
||||
_ = gzipCmd.Start()
|
||||
_ = cmd.Run()
|
||||
_ = gzipCmd.Wait()
|
||||
|
||||
if err := backupRepo.CreateRecord(&model.BackupRecord{
|
||||
Type: "database-mysql",
|
||||
Name: app.Name,
|
||||
DetailName: db.DBName,
|
||||
Source: "LOCAL",
|
||||
FileDir: backupDir,
|
||||
FileName: strings.ReplaceAll(backupName, backupDir, ""),
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) Recover(db dto.RecoverDB) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gzipFile, err := os.Open(db.BackupName)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer gzipFile.Close()
|
||||
gzipReader, err := gzip.NewReader(gzipFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
cmd := exec.Command("docker", "exec", "-i", app.ContainerName, "mysql", "-uroot", "-p"+app.Password, db.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) Delete(version string, ids []uint) error {
|
||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
||||
if err != nil {
|
||||
@ -330,13 +413,13 @@ func (u *MysqlService) LoadStatus(version string) (*dto.MysqlStatus, error) {
|
||||
}
|
||||
|
||||
func excuteSqlForMaps(containerName, password, command string) (map[string]string, error) {
|
||||
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
|
||||
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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 nil, errors.New(stdStr)
|
||||
}
|
||||
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
rows := strings.Split(stdStr, "\n")
|
||||
rowMap := make(map[string]string)
|
||||
for _, v := range rows {
|
||||
@ -349,25 +432,21 @@ func excuteSqlForMaps(containerName, password, command string) (map[string]strin
|
||||
}
|
||||
|
||||
func excuteSqlForRows(containerName, password, command string) ([]string, error) {
|
||||
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
|
||||
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 nil, errors.New(stdStr)
|
||||
}
|
||||
return strings.Split(stdStr, "\n"), nil
|
||||
}
|
||||
|
||||
func excuteSql(containerName, password, command string) error {
|
||||
cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command)
|
||||
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
|
||||
if strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
return errors.New(string(stdStr))
|
||||
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
return errors.New(stdStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,71 +1,30 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func TestMysql(t *testing.T) {
|
||||
cmd := exec.Command("docker", "exec", "-i", "1Panel-mysql5.7-RnzE", "mysql", "-uroot", "-pCalong@2016", "-e", "show global variables;")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
kk := strings.Split(string(stdout), "\n")
|
||||
testMap := make(map[string]interface{})
|
||||
for _, v := range kk {
|
||||
itemRow := strings.Split(v, "\t")
|
||||
if len(itemRow) == 2 {
|
||||
testMap[itemRow[0]] = itemRow[1]
|
||||
}
|
||||
}
|
||||
var info dto.MysqlVariables
|
||||
arr, err := json.Marshal(testMap)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
_ = json.Unmarshal(arr, &info)
|
||||
fmt.Print(info)
|
||||
// fmt.Println(string(stdout))
|
||||
// for {
|
||||
// str, err := hr.Reader.ReadString('\n')
|
||||
// if err == nil {
|
||||
// testMap := make(map[string]interface{})
|
||||
// err = json.Unmarshal([]byte(str), &testMap)
|
||||
// fmt.Println(err)
|
||||
// for k, v := range testMap {
|
||||
// fmt.Println(k, v)
|
||||
// }
|
||||
// // fmt.Print(str)
|
||||
// } else if err == io.EOF {
|
||||
// // ReadString最后会同EOF和最后的数据一起返回
|
||||
// fmt.Println(str)
|
||||
// break
|
||||
// } else {
|
||||
// fmt.Println("出错!!")
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// input, err := hr.Reader.ReadString('\n')
|
||||
// if err == nil {
|
||||
// fmt.Printf("The input was: %s\n", input)
|
||||
// }
|
||||
|
||||
// _, err = hr.Conn.Write([]byte("show global variables; \n"))
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// time.Sleep(3 * time.Second)
|
||||
// buf1 := make([]byte, 1024)
|
||||
// _, err = hr.Reader.Read(buf1)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// fmt.Println(string(buf1))
|
||||
gzipFile, err := os.Open("/tmp/ko.sql.gz")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer gzipFile.Close()
|
||||
gzipReader, err := gzip.NewReader(gzipFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
cmd := exec.Command("docker", "exec", "-i", "365", "mysql", "-uroot", "-pCalong@2012", "kubeoperator")
|
||||
cmd.Stdin = gzipReader
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
fmt.Println(string(stdout), err)
|
||||
}
|
||||
|
@ -8,4 +8,7 @@ const (
|
||||
OSS = "OSS"
|
||||
Sftp = "SFTP"
|
||||
MinIo = "MINIO"
|
||||
|
||||
DatabaseDir = "/opt/1Panel/data/backup/database"
|
||||
WebsiteDir = "/opt/1Panel/data/backup/website"
|
||||
)
|
@ -17,7 +17,4 @@ const (
|
||||
DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json"
|
||||
TmpDockerBuildDir = "/opt/1Panel/data/docker/build"
|
||||
TmpComposeBuildDir = "/opt/1Panel/data/docker/compose"
|
||||
|
||||
ExecCmd = "docker exec"
|
||||
ExecCmdIT = "docker exec -it"
|
||||
)
|
||||
|
@ -126,7 +126,7 @@ var AddTableSetting = &gormigrate.Migration{
|
||||
var AddTableBackupAccount = &gormigrate.Migration{
|
||||
ID: "20200916-add-table-backup",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.BackupAccount{}); err != nil {
|
||||
if err := tx.AutoMigrate(&model.BackupAccount{}, &model.BackupRecord{}); err != nil {
|
||||
return err
|
||||
}
|
||||
item := &model.BackupAccount{
|
||||
|
@ -25,6 +25,7 @@ func (s *BackupRouter) InitBackupRouter(Router *gin.RouterGroup) {
|
||||
baRouter.POST("/buckets", baseApi.ListBuckets)
|
||||
withRecordRouter.POST("", baseApi.CreateBackup)
|
||||
withRecordRouter.POST("/del", baseApi.DeleteBackup)
|
||||
withRecordRouter.POST("/record/del", baseApi.DeleteBackupRecord)
|
||||
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
|
||||
{
|
||||
withRecordRouter.POST("", baseApi.CreateMysql)
|
||||
withRecordRouter.PUT("/:id", baseApi.UpdateMysql)
|
||||
withRecordRouter.POST("/backup", baseApi.BackupMysql)
|
||||
withRecordRouter.POST("/recover", baseApi.RecoverMysql)
|
||||
withRecordRouter.POST("/backups/search", baseApi.SearchDBBackups)
|
||||
withRecordRouter.POST("/del", baseApi.DeleteMysql)
|
||||
withRecordRouter.POST("/variables/update", baseApi.UpdateMysqlVariables)
|
||||
cmdRouter.POST("/search", baseApi.SearchMysql)
|
||||
|
@ -13,6 +13,13 @@ export namespace Backup {
|
||||
credential: string;
|
||||
vars: string;
|
||||
}
|
||||
export interface RecordInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
source: string;
|
||||
fileDir: string;
|
||||
fileName: string;
|
||||
}
|
||||
export interface ForBucket {
|
||||
type: string;
|
||||
credential: string;
|
||||
|
@ -4,6 +4,19 @@ export namespace Database {
|
||||
export interface Search extends ReqPage {
|
||||
version: string;
|
||||
}
|
||||
export interface SearchBackupRecord extends ReqPage {
|
||||
version: string;
|
||||
dbName: string;
|
||||
}
|
||||
export interface Backup {
|
||||
version: string;
|
||||
dbName: string;
|
||||
}
|
||||
export interface Recover {
|
||||
version: string;
|
||||
dbName: string;
|
||||
backupName: string;
|
||||
}
|
||||
export interface MysqlDBInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
|
@ -16,6 +16,9 @@ export const editBackup = (params: Backup.BackupOperate) => {
|
||||
export const deleteBackup = (params: { ids: number[] }) => {
|
||||
return http.post(`/backups/del`, params);
|
||||
};
|
||||
export const deleteBackupRecord = (params: { ids: number[] }) => {
|
||||
return http.post(`/backups/record/del`, params);
|
||||
};
|
||||
|
||||
export const listBucket = (params: Backup.ForBucket) => {
|
||||
return http.post(`/backups/buckets`, params);
|
||||
|
@ -1,11 +1,22 @@
|
||||
import http from '@/api';
|
||||
import { ResPage } from '../interface';
|
||||
import { Backup } from '../interface/backup';
|
||||
import { Database } from '../interface/database';
|
||||
|
||||
export const searchMysqlDBs = (params: Database.Search) => {
|
||||
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 searchBackupRecords = (params: Database.SearchBackupRecord) => {
|
||||
return http.post<ResPage<Backup.RecordInfo>>(`/databases/backups/search`, params);
|
||||
};
|
||||
|
||||
export const addMysqlDB = (params: Database.MysqlDBCreate) => {
|
||||
return http.post(`/databases`, params);
|
||||
};
|
||||
|
@ -152,6 +152,8 @@ export default {
|
||||
logout: '退出登录',
|
||||
},
|
||||
database: {
|
||||
source: '来源',
|
||||
backup: '备份数据库',
|
||||
permission: '权限',
|
||||
permissionLocal: '本地服务器',
|
||||
permissionForIP: '指定 IP',
|
||||
|
116
frontend/src/views/database/mysql/backup/index.vue
Normal file
116
frontend/src/views/database/mysql/backup/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog v-model="backupVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('database.backup') }} - {{ dbName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<ComplexTable :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 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('commons.table.name')" prop="fileName" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('database.source')" prop="source" />
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFromat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
import { backup, searchBackupRecords } from '@/api/modules/database';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { deleteBackupRecord } from '@/api/modules/backup';
|
||||
import { Backup } from '@/api/interface/backup';
|
||||
|
||||
const selects = ref<any>([]);
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const backupVisiable = ref(false);
|
||||
const version = ref();
|
||||
const dbName = ref();
|
||||
interface DialogProps {
|
||||
version: string;
|
||||
dbName: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
version.value = params.version;
|
||||
dbName.value = params.dbName;
|
||||
backupVisiable.value = true;
|
||||
search();
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
let params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
version: version.value,
|
||||
dbName: dbName.value,
|
||||
};
|
||||
const res = await searchBackupRecords(params);
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const onBackup = async () => {
|
||||
let params = {
|
||||
version: version.value,
|
||||
dbName: dbName.value,
|
||||
};
|
||||
await backup(params);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
};
|
||||
|
||||
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', true);
|
||||
search();
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
icon: 'Delete',
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
onBatchDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -107,12 +107,14 @@
|
||||
</el-dialog>
|
||||
|
||||
<OperatrDialog @search="search" ref="dialogRef" />
|
||||
<BackupRecords ref="dialogBackupRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import OperatrDialog from '@/views/database/mysql/create/index.vue';
|
||||
import BackupRecords from '@/views/database/mysql/backup/index.vue';
|
||||
import Setting from '@/views/database/mysql/setting/index.vue';
|
||||
import Submenu from '@/views/database/index.vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
@ -144,6 +146,15 @@ const onOpenDialog = async () => {
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const dialogBackupRef = ref();
|
||||
const onOpenBackupDialog = async (dbName: string) => {
|
||||
let params = {
|
||||
version: version.value,
|
||||
dbName: dbName,
|
||||
};
|
||||
dialogBackupRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const settingRef = ref();
|
||||
const onSetting = async () => {
|
||||
isOnSetting.value = true;
|
||||
@ -252,9 +263,9 @@ const buttons = [
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('database.backupList') + '(1)',
|
||||
label: i18n.global.t('database.backupList'),
|
||||
click: (row: Database.MysqlDBInfo) => {
|
||||
onBatchDelete(row);
|
||||
onOpenBackupDialog(row.name);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user