diff --git a/backend/utils/mysql/client.go b/backend/utils/mysql/client.go index ffc5978d9..e5d27e25f 100644 --- a/backend/utils/mysql/client.go +++ b/backend/utils/mysql/client.go @@ -17,6 +17,9 @@ type MysqlClient interface { ChangePassword(info client.PasswordChangeInfo) error ChangeAccess(info client.AccessChangeInfo) error + Backup(info client.BackupInfo) (string, error) + Recover(info client.RecoverInfo) error + Close() } @@ -26,7 +29,7 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { return nil, buserr.New(constant.ErrCmdIllegal) } connArgs := []string{"exec", conn.Address, "mysql", "-u" + conn.Username, "-p" + conn.Password, "-e"} - return client.NewLocal(connArgs, conn.Address), nil + return client.NewLocal(connArgs, conn.Address, conn.Password), nil } connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", conn.Username, conn.Password, conn.Address, conn.Port) @@ -37,5 +40,11 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { if err := db.Ping(); err != nil { return nil, err } - return client.NewRemote(db), nil + return client.NewRemote(client.Remote{ + Client: db, + User: conn.Username, + Password: conn.Password, + Address: conn.Address, + Port: conn.Port, + }), nil } diff --git a/backend/utils/mysql/client/info.go b/backend/utils/mysql/client/info.go index 5eed4f6d7..415b1a8b1 100644 --- a/backend/utils/mysql/client/info.go +++ b/backend/utils/mysql/client/info.go @@ -52,6 +52,22 @@ type AccessChangeInfo struct { Timeout uint `json:"timeout"` // second } +type BackupInfo struct { + Name string `json:"name"` + Format string `json:"format"` + TargetDir string `json:"targetDir"` + + Timeout uint `json:"timeout"` // second +} + +type RecoverInfo struct { + Name string `json:"name"` + Format string `json:"format"` + SourceFile string `json:"sourceFile"` + + Timeout uint `json:"timeout"` // second +} + var formatMap = map[string]string{ "utf8": "utf8_general_ci", "utf8mb4": "utf8mb4_general_ci", diff --git a/backend/utils/mysql/client/local.go b/backend/utils/mysql/client/local.go index d0d8ae49e..788776a66 100644 --- a/backend/utils/mysql/client/local.go +++ b/backend/utils/mysql/client/local.go @@ -1,9 +1,11 @@ package client import ( + "compress/gzip" "context" "errors" "fmt" + "os" "os/exec" "strings" "time" @@ -11,15 +13,17 @@ import ( "github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/files" ) type Local struct { PrefixCommand []string + Password string ContainerName string } -func NewLocal(command []string, containerName string) *Local { - return &Local{PrefixCommand: command, ContainerName: containerName} +func NewLocal(command []string, containerName, password string) *Local { + return &Local{PrefixCommand: command, ContainerName: containerName, Password: password} } func (r *Local) Create(info CreateInfo) error { @@ -201,6 +205,54 @@ func (r *Local) ChangeAccess(info AccessChangeInfo) error { return nil } +func (r *Local) Backup(info BackupInfo) (string, error) { + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return "", fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + fileName := fmt.Sprintf("%s/%s_%s.sql.gz", info.TargetDir, info.Name, time.Now().Format("20060102150405")) + outfile, _ := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0755) + global.LOG.Infof("start to mysqldump | gzip > %s.gzip", info.TargetDir+"/"+fileName) + cmd := exec.Command("docker", "exec", r.ContainerName, "mysqldump", "-uroot", "-p"+r.Password, info.Name) + gzipCmd := exec.Command("gzip", "-cf") + gzipCmd.Stdin, _ = cmd.StdoutPipe() + gzipCmd.Stdout = outfile + _ = gzipCmd.Start() + _ = cmd.Run() + _ = gzipCmd.Wait() + return fileName, nil +} + +func (r *Local) Recover(info RecoverInfo) error { + fi, _ := os.Open(info.SourceFile) + defer fi.Close() + cmd := exec.Command("docker", "exec", "-i", r.ContainerName, "mysql", "-uroot", "-p"+r.Password, info.Name) + if strings.HasSuffix(info.SourceFile, ".gz") { + gzipFile, err := os.Open(info.SourceFile) + 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 +} + func (r *Local) Close() {} func (r *Local) ExecSQL(command string, timeout uint) error { diff --git a/backend/utils/mysql/client/remote.go b/backend/utils/mysql/client/remote.go index f97a46e86..16e40c978 100644 --- a/backend/utils/mysql/client/remote.go +++ b/backend/utils/mysql/client/remote.go @@ -4,20 +4,29 @@ import ( "context" "database/sql" "fmt" + "os" + "path" "strings" "time" "github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/files" + + "github.com/jarvanstack/mysqldump" ) type Remote struct { - Client *sql.DB + Client *sql.DB + User string + Password string + Address string + Port uint } -func NewRemote(client *sql.DB) *Remote { - return &Remote{Client: client} +func NewRemote(db Remote) *Remote { + return &db } func (r *Remote) Create(info CreateInfo) error { @@ -199,6 +208,55 @@ func (r *Remote) ChangeAccess(info AccessChangeInfo) error { return nil } +func (r *Remote) Backup(info BackupInfo) (string, error) { + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return "", fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + fileName := fmt.Sprintf("%s/%s_%s.sql", info.TargetDir, info.Name, time.Now().Format("20060102150405")) + dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F") + + f, _ := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0755) + defer f.Close() + if err := mysqldump.Dump(dns, mysqldump.WithData(), mysqldump.WithWriter(f)); err != nil { + return "", err + } + + if err := fileOp.Compress([]string{fileName}, info.TargetDir, path.Base(fileName)+".gz", files.Gz); err != nil { + return "", err + } + return fileName, nil +} + +func (r *Remote) Recover(info RecoverInfo) error { + fileOp := files.NewFileOp() + fileName := info.SourceFile + if strings.HasSuffix(info.SourceFile, ".sql.gz") { + fileName = strings.TrimSuffix(info.SourceFile, ".gz") + if err := fileOp.Decompress(info.SourceFile, fileName, files.Gz); err != nil { + return err + } + } + if strings.HasSuffix(info.SourceFile, ".tar.gz") { + fileName = strings.TrimSuffix(info.SourceFile, ".tar.gz") + if err := fileOp.Decompress(info.SourceFile, fileName, files.TarGz); err != nil { + return err + } + } + dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F") + f, err := os.Open(fileName) + if err != nil { + return err + } + defer f.Close() + if err := mysqldump.Source(dns, f); err != nil { + return err + } + return nil +} + func (r *Remote) Close() { _ = r.Client.Close() } diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index cb75610e0..7cf8ccc46 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -342,6 +342,9 @@ const message = { localDB: 'Local DB', address: 'DB address', version: 'DB version', + versionHelper: 'Currently, only versions 5.6, 5.7, and 8.0 are supported', + addressHelper: 'The remote database address except 127.0.0.1.', + userHelper: 'The root user or a database user with root privileges can access the remote database.', selectFile: 'Select file', dropHelper: 'You can drag and drop the uploaded file here or', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 87c2a775a..19024c342 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -337,6 +337,9 @@ const message = { localDB: '本地數據庫', address: '數據庫地址', version: '數據庫版本', + versionHelper: '當前僅支持 5.6 5.7 8.0 三個版本', + addressHelper: '非 127.0.0.1 的遠程數據庫地址', + userHelper: 'root 用戶或者擁有 root 權限的數據庫用戶', selectFile: '選擇文件', dropHelper: '將上傳文件拖拽到此處,或者', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 092a13887..f7a6b73aa 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -337,6 +337,9 @@ const message = { localDB: '本地数据库', address: '数据库地址', version: '数据库版本', + versionHelper: '当前仅支持 5.6 5.7 8.0 三个版本', + addressHelper: '非 127.0.0.1 的远程数据库地址', + userHelper: 'root 用户或者拥有 root 权限的数据库用户', selectFile: '选择文件', dropHelper: '将上传文件拖拽到此处,或者', diff --git a/frontend/src/views/database/mysql/create/index.vue b/frontend/src/views/database/mysql/create/index.vue index 4e18b8b58..2be1cfb1f 100644 --- a/frontend/src/views/database/mysql/create/index.vue +++ b/frontend/src/views/database/mysql/create/index.vue @@ -46,7 +46,7 @@ {{ $t('database.remoteHelper') }} - + ; const formRef = ref(); diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index fc5046f10..e22f6f288 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -13,7 +13,7 @@ -