mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
Merge pull request #56 from kanzihuang/feature-export-databases
feat: 批量导出数据库(仅支持 MySQL 数据库)
This commit is contained in:
@@ -80,8 +80,8 @@
|
|||||||
|
|
||||||
<template #more="{ data }">
|
<template #more="{ data }">
|
||||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||||
|
|
||||||
<el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
|
<el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
|
||||||
|
<el-button v-if="data.type=='mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
@@ -180,6 +180,37 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="9">
|
||||||
|
<el-form-item label="导出内容: " size="small">
|
||||||
|
<el-checkbox-group v-model="exportDialog.contents" :min=1>
|
||||||
|
<el-checkbox label="结构" />
|
||||||
|
<el-checkbox label="数据" />
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="9" >
|
||||||
|
<el-form-item label="扩展名: " size="small">
|
||||||
|
<el-radio-group v-model="exportDialog.extName">
|
||||||
|
<el-radio label="sql" />
|
||||||
|
<el-radio label="gz" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-transfer :titles="['全部数据库', '导出数据库']" max-height="300" size="small" v-model="exportDialog.value" :data="exportDialog.data">
|
||||||
|
</el-transfer>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div style="text-align: right">
|
||||||
|
<el-button @click="exportDialog.visible = false" size="small">取消</el-button>
|
||||||
|
<el-button @click="dumpDbs()" type="success" size="small">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
width="90%"
|
width="90%"
|
||||||
:title="`${sqlExecLogDialog.title} - SQL执行记录`"
|
:title="`${sqlExecLogDialog.title} - SQL执行记录`"
|
||||||
@@ -313,7 +344,7 @@ const columns = ref([
|
|||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('database', '数据库').isSlot().setMinWidth(70),
|
TableColumn.new('database', '数据库').isSlot().setMinWidth(70),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
TableColumn.new('more', '更多').isSlot().setMinWidth(165).fixedRight(),
|
TableColumn.new('more', '更多').isSlot().setMinWidth(220).fixedRight(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
@@ -402,6 +433,17 @@ const state = reactive({
|
|||||||
tableNameSearch: '',
|
tableNameSearch: '',
|
||||||
tableCommentSearch: '',
|
tableCommentSearch: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
exportDialog: {
|
||||||
|
visible: false,
|
||||||
|
dbId: 0,
|
||||||
|
type: 3,
|
||||||
|
data: [],
|
||||||
|
value: [],
|
||||||
|
contents: [],
|
||||||
|
extName: '',
|
||||||
|
},
|
||||||
|
|
||||||
columnDialog: {
|
columnDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
columns: [],
|
columns: [],
|
||||||
@@ -456,6 +498,7 @@ const {
|
|||||||
rollbackSqlDialog,
|
rollbackSqlDialog,
|
||||||
chooseTableName,
|
chooseTableName,
|
||||||
tableInfoDialog,
|
tableInfoDialog,
|
||||||
|
exportDialog,
|
||||||
columnDialog,
|
columnDialog,
|
||||||
indexDialog,
|
indexDialog,
|
||||||
ddlDialog,
|
ddlDialog,
|
||||||
@@ -613,6 +656,47 @@ const dump = (db: string) => {
|
|||||||
state.showDumpInfo = false;
|
state.showDumpInfo = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDumpDbs = async (row: any) => {
|
||||||
|
const dbs = row.database.split(' ');
|
||||||
|
const data = []
|
||||||
|
for (let name of dbs) {
|
||||||
|
data.push({
|
||||||
|
key: name,
|
||||||
|
label: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
state.exportDialog.value = []
|
||||||
|
state.exportDialog.data = data
|
||||||
|
state.exportDialog.dbId = row.id;
|
||||||
|
state.exportDialog.contents = ["结构", "数据"]
|
||||||
|
state.exportDialog.extName = "sql"
|
||||||
|
state.exportDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库信息导出
|
||||||
|
*/
|
||||||
|
const dumpDbs = () => {
|
||||||
|
isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
let type = 0
|
||||||
|
for (let c of state.exportDialog.contents) {
|
||||||
|
if (c == "结构") {
|
||||||
|
type += 1
|
||||||
|
} else if (c == "数据") {
|
||||||
|
type += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.setAttribute(
|
||||||
|
'href',
|
||||||
|
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${state.exportDialog.extName}&token=${getSession(
|
||||||
|
'token'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
a.click();
|
||||||
|
state.exportDialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
const onShowRollbackSql = async (sqlExecLog: any) => {
|
const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||||
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
|
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
|
||||||
const primaryKey = getPrimaryKey(columns);
|
const primaryKey = getPrimaryKey(columns);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mayfly-go/internal/db/api/form"
|
"mayfly-go/internal/db/api/form"
|
||||||
@@ -32,6 +33,18 @@ type Db struct {
|
|||||||
TagApp tagapp.TagTree
|
TagApp tagapp.TagTree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
writer *gzip.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g gzipResponseWriter) WriteString(data string) {
|
||||||
|
g.writer.Write([]byte(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g gzipResponseWriter) Close() {
|
||||||
|
g.writer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_ROW_SIZE = 5000
|
const DEFAULT_ROW_SIZE = 5000
|
||||||
|
|
||||||
// @router /api/dbs [get]
|
// @router /api/dbs [get]
|
||||||
@@ -198,6 +211,10 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] 解析SQL失败: [%s]", filename, dbConn.Info.GetLogDesc(), err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
sql := sqlparser.String(stmt)
|
sql := sqlparser.String(stmt)
|
||||||
execReq.Sql = sql
|
execReq.Sql = sql
|
||||||
// 需要记录执行记录
|
// 需要记录执行记录
|
||||||
@@ -219,38 +236,81 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
|||||||
// 数据库dump
|
// 数据库dump
|
||||||
func (d *Db) DumpSql(rc *req.Ctx) {
|
func (d *Db) DumpSql(rc *req.Ctx) {
|
||||||
g := rc.GinCtx
|
g := rc.GinCtx
|
||||||
db := getDbName(g)
|
dbId := getDbId(g)
|
||||||
|
dbNamesStr := g.Query("db")
|
||||||
dumpType := g.Query("type")
|
dumpType := g.Query("type")
|
||||||
tablesStr := g.Query("tables")
|
tablesStr := g.Query("tables")
|
||||||
biz.NotEmpty(tablesStr, "请选择要导出的表")
|
extName := g.Query("extName")
|
||||||
tables := strings.Split(tablesStr, ",")
|
switch extName {
|
||||||
|
case ".gz", ".gzip", "gz", "gzip":
|
||||||
|
extName = ".gz"
|
||||||
|
default:
|
||||||
|
extName = ""
|
||||||
|
}
|
||||||
|
|
||||||
// 是否需要导出表结构
|
// 是否需要导出表结构
|
||||||
needStruct := dumpType == "1" || dumpType == "3"
|
needStruct := dumpType == "1" || dumpType == "3"
|
||||||
// 是否需要导出数据
|
// 是否需要导出数据
|
||||||
needData := dumpType == "2" || dumpType == "3"
|
needData := dumpType == "2" || dumpType == "3"
|
||||||
|
|
||||||
dbInstance := d.getDbConnection(rc.GinCtx)
|
db := d.DbApp.GetById(dbId)
|
||||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbInstance.Info.TagPath), "%s")
|
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, db.TagPath), "%s")
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
filename := fmt.Sprintf("%s.%s.sql", db, now.Format("200601021504"))
|
filename := fmt.Sprintf("%s.%s.sql%s", db.Name, now.Format("20060102150405"), extName)
|
||||||
g.Header("Content-Type", "application/octet-stream")
|
g.Header("Content-Type", "application/octet-stream")
|
||||||
g.Header("Content-Disposition", "attachment; filename="+filename)
|
g.Header("Content-Disposition", "attachment; filename="+filename)
|
||||||
|
if extName != ".gz" {
|
||||||
|
g.Header("Content-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
|
||||||
writer := g.Writer
|
var dbNames, tables []string
|
||||||
|
if len(dbNamesStr) > 0 {
|
||||||
|
dbNames = strings.Split(dbNamesStr, ",")
|
||||||
|
}
|
||||||
|
if len(dbNames) == 1 && len(tablesStr) > 0 {
|
||||||
|
tables = strings.Split(tablesStr, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := gzipResponseWriter{writer: gzip.NewWriter(g.Writer)}
|
||||||
|
defer writer.Close()
|
||||||
|
for _, dbName := range dbNames {
|
||||||
|
d.dumpDb(writer, dbId, dbName, tables, needStruct, needData, len(dbNames) > 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.ReqParam = fmt.Sprintf("DB[id=%d, tag=%s, name=%s, databases=%s, tables=%s, dumpType=%s]", db.Id, db.TagPath, db.Name, dbNamesStr, tablesStr, dumpType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Db) dumpDb(writer gzipResponseWriter, dbId uint64, dbName string, tables []string, needStruct bool, needData bool, switchDb bool) {
|
||||||
|
dbConn := d.DbApp.GetDbConnection(dbId, dbName)
|
||||||
writer.WriteString("-- ----------------------------")
|
writer.WriteString("-- ----------------------------")
|
||||||
writer.WriteString("\n-- 导出平台: mayfly-go")
|
writer.WriteString("\n-- 导出平台: mayfly-go")
|
||||||
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", now.Format("2006-01-02 15:04:05")))
|
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", db))
|
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", dbName))
|
||||||
writer.WriteString("\n-- ----------------------------\n")
|
writer.WriteString("\n-- ----------------------------\n")
|
||||||
|
|
||||||
dbmeta := d.getDbConnection(rc.GinCtx).GetMeta()
|
if switchDb {
|
||||||
|
switch dbConn.Info.Type {
|
||||||
|
case entity.DbTypeMysql:
|
||||||
|
writer.WriteString(fmt.Sprintf("use `%s`;\n", dbName))
|
||||||
|
default:
|
||||||
|
biz.IsTrue(false, "数据库类型必须为 %s", entity.DbTypeMysql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbMeta := dbConn.GetMeta()
|
||||||
|
if len(tables) == 0 {
|
||||||
|
ti := dbMeta.GetTableInfos()
|
||||||
|
tables = make([]string, len(ti))
|
||||||
|
for i, table := range ti {
|
||||||
|
tables[i] = table.TableName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
if needStruct {
|
if needStruct {
|
||||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
|
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
|
||||||
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table))
|
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table))
|
||||||
writer.WriteString(dbmeta.GetCreateTableDdl(table) + ";\n")
|
writer.WriteString(dbMeta.GetCreateTableDdl(table) + ";\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !needData {
|
if !needData {
|
||||||
@@ -262,7 +322,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
|||||||
|
|
||||||
pageNum := 1
|
pageNum := 1
|
||||||
for {
|
for {
|
||||||
columns, result, _ := dbmeta.GetTableRecord(table, pageNum, DEFAULT_ROW_SIZE)
|
columns, result, _ := dbMeta.GetTableRecord(table, pageNum, DEFAULT_ROW_SIZE)
|
||||||
resultLen := len(result)
|
resultLen := len(result)
|
||||||
if resultLen == 0 {
|
if resultLen == 0 {
|
||||||
break
|
break
|
||||||
@@ -293,8 +353,6 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
|||||||
|
|
||||||
writer.WriteString("COMMIT;\n")
|
writer.WriteString("COMMIT;\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.ReqParam = fmt.Sprintf("%s, tables: %s, dumpType: %s", dbInstance.Info.GetLogDesc(), tablesStr, dumpType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @router /api/db/:dbId/t-metadata [get]
|
// @router /api/db/:dbId/t-metadata [get]
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func (d *dbAppImpl) GetDbConnection(dbId uint64, dbName string) *DbConnection {
|
|||||||
|
|
||||||
db := d.GetById(dbId)
|
db := d.GetById(dbId)
|
||||||
biz.NotNil(db, "数据库信息不存在")
|
biz.NotNil(db, "数据库信息不存在")
|
||||||
biz.IsTrue(strings.Contains(" "+db.Database+" ", " "+dbName+" "), "未配置该库的操作权限")
|
biz.IsTrue(strings.Contains(" "+db.Database+" ", " "+dbName+" "), "未配置数据库【%s】的操作权限", dbName)
|
||||||
|
|
||||||
instance := d.dbInstanceApp.GetById(db.InstanceId)
|
instance := d.dbInstanceApp.GetById(db.InstanceId)
|
||||||
// 密码解密
|
// 密码解密
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ func (mm *MysqlMetadata) GetTableIndex(tableName string) []Index {
|
|||||||
|
|
||||||
// 获取建表ddl
|
// 获取建表ddl
|
||||||
func (mm *MysqlMetadata) GetCreateTableDdl(tableName string) string {
|
func (mm *MysqlMetadata) GetCreateTableDdl(tableName string) string {
|
||||||
_, res, _ := mm.di.SelectData(fmt.Sprintf("show create table %s ", tableName))
|
_, res, err := mm.di.SelectData(fmt.Sprintf("show create table `%s` ", tableName))
|
||||||
|
biz.ErrIsNilAppendErr(err, "获取表结构失败: %s")
|
||||||
return res[0]["Create Table"].(string)
|
return res[0]["Create Table"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user