mirror of
				https://github.com/1Panel-dev/1Panel.git
				synced 2025-10-31 11:15:58 +08:00 
			
		
		
		
	feat: 完成 mysql 数据库备份与恢复功能
This commit is contained in:
		
							parent
							
								
									ad110054e4
								
							
						
					
					
						commit
						f0d82f9004
					
				
					 21 changed files with 494 additions and 82 deletions
				
			
		|  | @ -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…
	
	Add table
		
		Reference in a new issue