feat: 增加远程数据库提示信息 (#1745)

This commit is contained in:
ssongliu 2023-07-25 17:08:13 +08:00 committed by GitHub
parent bbd649188a
commit da4c264908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 189 additions and 38 deletions

View File

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

View File

@ -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",

View File

@ -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 {

View File

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

View File

@ -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',

View File

@ -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: '將上傳文件拖拽到此處或者',

View File

@ -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: '将上传文件拖拽到此处或者',

View File

@ -46,7 +46,7 @@
<span class="input-help">{{ $t('database.remoteHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-form-item :label="$t('commons.table.type')" prop="from">
<el-select v-model="form.from">
<el-option
v-for="(item, index) in dbOptions"
@ -108,6 +108,7 @@ const rules = reactive({
password: [Rules.requiredInput],
permission: [Rules.requiredSelect],
permissionIPs: [Rules.requiredInput],
from: [Rules.requiredSelect],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();

View File

@ -13,7 +13,7 @@
<template v-if="!isOnSetting" #search>
<el-select v-model="paginationConfig.from" @change="search()">
<template #prefix>{{ $t('website.group') }}</template>
<template #prefix>{{ $t('commons.table.type') }}</template>
<el-option-group>
<el-option :label="$t('database.localDB')" value="local" />
</el-option-group>
@ -28,19 +28,34 @@
</el-select>
</template>
<template #toolbar v-if="(mysqlIsExist && !isOnSetting) || !isLocal()">
<el-row :class="{ mask: mysqlStatus != 'Running' && isLocal() }">
<template #toolbar v-if="!isOnSetting">
<el-row>
<el-col :xs="24" :sm="20" :md="20" :lg="20" :xl="20">
<el-button type="primary" @click="onOpenDialog()">
<el-button
v-if="(mysqlIsExist && mysqlStatus === 'Running') || !isLocal()"
type="primary"
@click="onOpenDialog()"
>
{{ $t('database.create') }}
</el-button>
<el-button v-if="isLocal()" @click="onChangeRootPassword" type="primary" plain>
<el-button
v-if="mysqlIsExist && mysqlStatus === 'Running' && isLocal()"
@click="onChangeRootPassword"
type="primary"
plain
>
{{ $t('database.databaseConnInfo') }}
</el-button>
<el-button @click="goRemoteDB" type="primary" plain>
{{ $t('database.remoteDB') }}
</el-button>
<el-button v-if="isLocal()" @click="goDashboard" icon="Position" type="primary" plain>
<el-button
v-if="mysqlIsExist && mysqlStatus === 'Running' && isLocal()"
@click="goDashboard"
icon="Position"
type="primary"
plain
>
phpMyAdmin
</el-button>
</el-col>

View File

@ -131,7 +131,7 @@ const onOpenDialog = async (
rowData: Partial<Database.RemoteDBInfo> = {
name: '',
type: 'mysql',
version: '5.6.x',
version: '5.6',
address: '',
port: 3306,
username: '',

View File

@ -1,5 +1,5 @@
<template>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="title" :resource="dialogData.rowData?.name" :back="handleClose" />
</template>
@ -11,26 +11,25 @@
</el-form-item>
<el-form-item :label="$t('database.version')" prop="version">
<el-select v-model="dialogData.rowData!.version">
<el-option value="5.6.x" label="5.6.x" />
<el-option value="5.7.x" label="5.7.x" />
<el-option value="8.0.x" label="8.0.x" />
<el-option value="5.6" label="5.6" />
<el-option value="5.7" label="5.7" />
<el-option value="8.0" label="8.0" />
</el-select>
<span class="input-help">{{ $t('database.versionHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('database.address')" prop="address">
<el-input clearable v-model.trim="dialogData.rowData!.address" />
<span class="input-help">{{ $t('database.addressHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input clearable v-model.trim="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input clearable v-model.trim="dialogData.rowData!.username" />
<span class="input-help">{{ $t('database.userHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model.trim="dialogData.rowData!.password">
<template #append>
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
<el-input type="password" clearable show-password v-model.trim="dialogData.rowData!.password" />
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable v-model.trim="dialogData.rowData!.description" />
@ -57,7 +56,6 @@ import { Database } from '@/api/interface/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { getRandomStr } from '@/utils/util';
import { addRemoteDB, editRemoteDB } from '@/api/modules/database';
interface DialogProps {
@ -93,10 +91,6 @@ const rules = reactive({
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const random = async () => {
dialogData.value.rowData!.password = getRandomStr(16);
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {

3
go.mod
View File

@ -21,11 +21,12 @@ require (
github.com/go-acme/lego/v4 v4.9.0
github.com/go-gormigrate/gormigrate/v2 v2.0.2
github.com/go-playground/validator/v10 v10.14.0
github.com/go-sql-driver/mysql v1.6.0
github.com/go-sql-driver/mysql v1.7.0
github.com/goh-chunlin/go-onedrive v1.1.1
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/jarvanstack/mysqldump v0.7.0
github.com/jinzhu/copier v0.3.5
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.16.5

12
go.sum
View File

@ -333,8 +333,8 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@ -507,6 +507,8 @@ github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=
github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
github.com/jarvanstack/mysqldump v0.7.0 h1:lspwQQhLrpVpCCbNxs5GPNTAcrqYTLVLIO7txUbtYdc=
github.com/jarvanstack/mysqldump v0.7.0/go.mod h1:NBXaxyEQjiaLwLP9s3pGfal7aZL/5omKJrk+bC1E250=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
@ -843,8 +845,6 @@ github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9J
github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q=
github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.4 h1:oGB351qH1JqUqK1tsMYEE5qTBbPk394BhsZxmUfebcI=
github.com/swaggo/swag v1.8.4/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@ -1011,8 +1011,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
@ -1204,8 +1202,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=