Files
mayfly-go/server/internal/db/api/db.go

499 lines
14 KiB
Go
Raw Normal View History

2021-09-11 14:04:09 +08:00
package api
2021-01-08 15:37:32 +08:00
import (
"context"
2023-12-06 09:23:23 +08:00
"encoding/base64"
2021-01-08 15:37:32 +08:00
"fmt"
"io"
"mayfly-go/internal/common/consts"
2022-09-09 18:26:08 +08:00
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/dbm"
2022-09-09 18:26:08 +08:00
"mayfly-go/internal/db/domain/entity"
2023-07-03 21:42:04 +08:00
msgapp "mayfly-go/internal/msg/application"
2023-10-10 17:39:46 +08:00
msgdto "mayfly-go/internal/msg/application/dto"
2022-10-26 20:49:29 +08:00
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
2023-01-14 16:29:52 +08:00
"mayfly-go/pkg/req"
2023-10-20 21:31:46 +08:00
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
2021-01-08 15:37:32 +08:00
"strconv"
2021-04-21 10:22:09 +08:00
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/kanzihuang/vitess/go/vt/sqlparser"
2021-04-16 15:10:07 +08:00
)
2021-01-08 15:37:32 +08:00
type Db struct {
InstanceApp application.Instance
DbApp application.Db
DbSqlExecApp application.DbSqlExec
2023-07-03 21:42:04 +08:00
MsgApp msgapp.Msg
2022-10-26 20:49:29 +08:00
TagApp tagapp.TagTree
}
2021-01-08 15:37:32 +08:00
// @router /api/dbs [get]
2023-01-14 16:29:52 +08:00
func (d *Db) Dbs(rc *req.Ctx) {
queryCond, page := ginx.BindQueryAndPage[*entity.DbQuery](rc.GinCtx, new(entity.DbQuery))
2022-10-26 20:49:29 +08:00
// 不存在可访问标签id即没有可操作数据
codes := d.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeDb, queryCond.TagPath)
if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]()
2022-10-26 20:49:29 +08:00
return
}
queryCond.Codes = codes
2023-10-31 12:36:04 +08:00
res, err := d.DbApp.GetPageList(queryCond, page, new([]vo.DbListVO))
biz.ErrIsNil(err)
rc.ResData = res
2021-01-08 15:37:32 +08:00
}
2023-01-14 16:29:52 +08:00
func (d *Db) Save(rc *req.Ctx) {
form := &form.DbForm{}
db := ginx.BindJsonAndCopyTo[*entity.Db](rc.GinCtx, form, new(entity.Db))
rc.ReqParam = form
biz.ErrIsNil(d.DbApp.Save(rc.MetaCtx, db, form.TagId...))
}
2023-01-14 16:29:52 +08:00
func (d *Db) DeleteDb(rc *req.Ctx) {
idsStr := ginx.PathParam(rc.GinCtx, "dbId")
rc.ReqParam = idsStr
ids := strings.Split(idsStr, ",")
ctx := rc.MetaCtx
for _, v := range ids {
value, err := strconv.Atoi(v)
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
dbId := uint64(value)
d.DbApp.Delete(ctx, dbId)
// 删除该库的sql执行记录
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
2023-12-27 22:59:20 +08:00
// todo delete restore task and histories
}
}
/** 数据库操作相关、执行sql等 ***/
2023-01-14 16:29:52 +08:00
func (d *Db) ExecSql(rc *req.Ctx) {
2021-04-16 15:10:07 +08:00
g := rc.GinCtx
form := &form.DbSqlExecForm{}
ginx.BindJsonAndValid(g, form)
dbId := getDbId(g)
dbConn, err := d.DbApp.GetDbConn(dbId, form.Db)
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
2023-12-06 09:23:23 +08:00
sqlBytes, err := base64.StdEncoding.DecodeString(form.Sql)
biz.ErrIsNilAppendErr(err, "sql解码失败: %s")
// 去除前后空格及换行符
sql := stringx.TrimSpaceAndBr(string(sqlBytes))
rc.ReqParam = fmt.Sprintf("%s %s\n-> %s", dbConn.Info.GetLogDesc(), form.ExecId, sql)
2022-11-02 19:27:40 +08:00
biz.NotEmpty(form.Sql, "sql不能为空")
2022-11-02 19:27:40 +08:00
execReq := &application.DbSqlExecReq{
DbId: dbId,
Db: form.Db,
Remark: form.Remark,
DbConn: dbConn,
}
// 比前端超时时间稍微快一点,可以提示到前端
ctx, cancel := context.WithTimeout(rc.MetaCtx, 58*time.Second)
defer cancel()
2022-11-02 19:27:40 +08:00
sqls, err := sqlparser.SplitStatementToPieces(sql, sqlparser.WithDialect(dbConn.Info.Type.Dialect()))
2023-06-17 08:59:37 +08:00
biz.ErrIsNil(err, "SQL解析错误,请检查您的执行SQL")
2022-11-02 19:27:40 +08:00
isMulti := len(sqls) > 1
var execResAll *application.DbSqlExecRes
2022-11-02 19:27:40 +08:00
for _, s := range sqls {
s = stringx.TrimSpaceAndBr(s)
2023-11-13 17:41:03 +08:00
// 多条执行,暂不支持查询语句
if isMulti {
biz.IsTrue(!strings.HasPrefix(strings.ToLower(s), "select"), "多条语句执行暂不不支持select语句")
2022-11-02 19:27:40 +08:00
}
2023-11-13 17:41:03 +08:00
2022-11-02 19:27:40 +08:00
execReq.Sql = s
execRes, err := d.DbSqlExecApp.Exec(ctx, execReq)
2023-11-13 17:41:03 +08:00
biz.ErrIsNilAppendErr(err, fmt.Sprintf("[%s] -> 执行失败: ", s)+"%s")
2022-11-02 19:27:40 +08:00
if execResAll == nil {
execResAll = execRes
} else {
execResAll.Merge(execRes)
}
}
colAndRes := make(map[string]any)
colAndRes["columns"] = execResAll.Columns
2022-11-02 19:27:40 +08:00
colAndRes["res"] = execResAll.Res
rc.ResData = colAndRes
2021-01-08 15:37:32 +08:00
}
// progressCategory sql文件执行进度消息类型
const progressCategory = "execSqlFileProgress"
// progressMsg sql文件执行进度消息
type progressMsg struct {
Id string `json:"id"`
Title string `json:"title"`
ExecutedStatements int `json:"executedStatements"`
Terminated bool `json:"terminated"`
}
// 执行sql文件
2023-01-14 16:29:52 +08:00
func (d *Db) ExecSqlFile(rc *req.Ctx) {
g := rc.GinCtx
multipart, err := g.Request.MultipartReader()
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
file, err := multipart.NextPart()
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
defer file.Close()
filename := file.FileName()
dbId := getDbId(g)
dbName := getDbName(g)
clientId := g.Query("clientId")
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
defer func() {
2023-10-20 21:31:46 +08:00
if err := recover(); err != nil {
errInfo := anyx.ToString(err)
if len(errInfo) > 300 {
errInfo = errInfo[:300] + "..."
}
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s]执行失败: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)).WithClientId(clientId))
}
}()
execReq := &application.DbSqlExecReq{
DbId: dbId,
Db: dbName,
Remark: filename,
DbConn: dbConn,
2022-12-17 22:24:21 +08:00
}
var sql string
tokenizer := sqlparser.NewReaderTokenizer(file,
sqlparser.WithCacheInBuffer(), sqlparser.WithDialect(dbConn.Info.Type.Dialect()))
executedStatements := 0
progressId := stringx.Rand(32)
laId := rc.GetLoginAccount().Id
defer ws.SendJsonMsg(ws.UserId(laId), clientId, msgdto.InfoSysMsg("sql脚本执行进度", &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: true,
}).WithCategory(progressCategory))
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ws.SendJsonMsg(ws.UserId(laId), clientId, msgdto.InfoSysMsg("sql脚本执行进度", &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
default:
}
executedStatements++
sql, err = sqlparser.SplitNext(tokenizer)
if err == io.EOF {
break
}
biz.ErrIsNilAppendErr(err, "%s")
const prefixUse = "use "
const prefixUSE = "USE "
if strings.HasPrefix(sql, prefixUSE) || strings.HasPrefix(sql, prefixUse) {
var stmt sqlparser.Statement
stmt, err = sqlparser.Parse(sql)
biz.ErrIsNilAppendErr(err, "%s")
stmtUse, ok := stmt.(*sqlparser.Use)
// 最终执行结果以数据库直接结果为准
if !ok {
logx.Warnf("sql解析失败: %s", sql)
2022-12-17 22:24:21 +08:00
}
dbConn, err = d.DbApp.GetDbConn(dbId, stmtUse.DBName.String())
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath...), "%s")
execReq.DbConn = dbConn
}
// 需要记录执行记录
const maxRecordStatements = 64
if executedStatements < maxRecordStatements {
execReq.Sql = sql
_, err = d.DbSqlExecApp.Exec(rc.MetaCtx, execReq)
} else {
_, err = dbConn.Exec(sql)
}
2022-12-17 22:24:21 +08:00
biz.ErrIsNilAppendErr(err, "%s")
}
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("sql脚本执行完成%s", rc.ReqParam)).WithClientId(clientId))
}
// 数据库dump
2023-01-14 16:29:52 +08:00
func (d *Db) DumpSql(rc *req.Ctx) {
g := rc.GinCtx
2023-08-31 20:05:38 +08:00
dbId := getDbId(g)
dbNamesStr := g.Query("db")
dumpType := g.Query("type")
tablesStr := g.Query("tables")
extName := g.Query("extName")
switch extName {
case ".gz", ".gzip", "gz", "gzip":
extName = ".gz"
default:
extName = ""
}
// 是否需要导出表结构
needStruct := dumpType == "1" || dumpType == "3"
// 是否需要导出数据
needData := dumpType == "2" || dumpType == "3"
la := rc.GetLoginAccount()
db, err := d.DbApp.GetById(new(entity.Db), dbId)
biz.ErrIsNil(err, "该数据库不存在")
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), "%s")
now := time.Now()
filename := fmt.Sprintf("%s.%s.sql%s", db.Name, now.Format("20060102150405"), extName)
g.Header("Content-Type", "application/octet-stream")
g.Header("Content-Disposition", "attachment; filename="+filename)
if extName != ".gz" {
g.Header("Content-Encoding", "gzip")
}
2023-08-31 20:05:38 +08:00
var dbNames, tables []string
if len(dbNamesStr) > 0 {
dbNames = strings.Split(dbNamesStr, ",")
}
if len(dbNames) == 1 && len(tablesStr) > 0 {
tables = strings.Split(tablesStr, ",")
}
2023-10-20 21:31:46 +08:00
2023-09-07 11:15:11 +08:00
writer := newGzipWriter(g.Writer)
defer func() {
2023-10-20 21:31:46 +08:00
msg := anyx.ToString(recover())
if len(msg) > 0 {
msg = "数据库导出失败: " + msg
writer.WriteString(msg)
d.MsgApp.CreateAndSend(la, msgdto.ErrSysMsg("数据库导出失败", msg))
}
writer.Close()
}()
2023-10-20 21:31:46 +08:00
2023-08-31 20:05:38 +08:00
for _, dbName := range dbNames {
2023-10-20 17:37:09 +08:00
d.dumpDb(writer, dbId, dbName, tables, needStruct, needData)
2023-08-31 20:05:38 +08:00
}
rc.ReqParam = collx.Kvs("db", db, "databases", dbNamesStr, "tables", tablesStr, "dumpType", dumpType)
2023-08-31 20:05:38 +08:00
}
2023-10-20 17:37:09 +08:00
func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []string, needStruct bool, needData bool) {
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
biz.ErrIsNil(err)
writer.WriteString("\n-- ----------------------------")
writer.WriteString("\n-- 导出平台: mayfly-go")
2023-08-31 20:05:38 +08:00
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", time.Now().Format("2006-01-02 15:04:05")))
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", dbName))
2023-10-20 17:37:09 +08:00
writer.WriteString("\n-- ----------------------------\n\n")
2023-10-20 17:37:09 +08:00
writer.WriteString(dbConn.Info.Type.StmtUseDatabase(dbName))
writer.WriteString(dbConn.Info.Type.StmtSetForeignKeyChecks(false))
2023-11-26 21:21:35 +08:00
dbMeta := dbConn.GetDialect()
2023-08-31 20:05:38 +08:00
if len(tables) == 0 {
ti, err := dbMeta.GetTables()
biz.ErrIsNil(err)
2023-08-31 20:05:38 +08:00
tables = make([]string, len(ti))
for i, table := range ti {
tables[i] = table.TableName
}
}
for _, table := range tables {
writer.TryFlush()
quotedTable := dbConn.Info.Type.QuoteIdentifier(table)
if needStruct {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS %s;\n", quotedTable))
2023-12-20 17:29:16 +08:00
ddl, err := dbMeta.GetTableDDL(table)
biz.ErrIsNil(err)
writer.WriteString(ddl + "\n")
}
if !needData {
continue
}
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", table))
writer.WriteString("BEGIN;\n")
insertSql := "INSERT INTO %s VALUES (%s);\n"
2024-01-06 22:36:50 +08:00
dbMeta.WalkTableRecord(table, func(record map[string]any, columns []*dbm.QueryColumn) error {
2023-09-07 11:15:11 +08:00
var values []string
writer.TryFlush()
2023-09-07 11:15:11 +08:00
for _, column := range columns {
value := record[column.Name]
2023-09-07 11:15:11 +08:00
if value == nil {
values = append(values, "NULL")
continue
}
strValue, ok := value.(string)
if ok {
strValue = dbConn.Info.Type.QuoteLiteral(strValue)
values = append(values, strValue)
2023-09-07 11:15:11 +08:00
} else {
2023-10-20 21:31:46 +08:00
values = append(values, anyx.ToString(value))
}
2022-12-17 22:24:21 +08:00
}
writer.WriteString(fmt.Sprintf(insertSql, quotedTable, strings.Join(values, ", ")))
2024-01-06 22:36:50 +08:00
return nil
2023-09-07 11:15:11 +08:00
})
writer.WriteString("COMMIT;\n")
}
writer.WriteString(dbConn.Info.Type.StmtSetForeignKeyChecks(true))
}
func (d *Db) TableInfos(rc *req.Ctx) {
2023-11-26 21:21:35 +08:00
res, err := d.getDbConn(rc.GinCtx).GetDialect().GetTables()
biz.ErrIsNilAppendErr(err, "获取表信息失败: %s")
rc.ResData = res
}
func (d *Db) TableIndex(rc *req.Ctx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
2023-11-26 21:21:35 +08:00
res, err := d.getDbConn(rc.GinCtx).GetDialect().GetTableIndex(tn)
biz.ErrIsNilAppendErr(err, "获取表索引信息失败: %s")
rc.ResData = res
2021-01-08 15:37:32 +08:00
}
// @router /api/db/:dbId/c-metadata [get]
2023-01-14 16:29:52 +08:00
func (d *Db) ColumnMA(rc *req.Ctx) {
2021-04-16 15:10:07 +08:00
g := rc.GinCtx
tn := g.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
dbi := d.getDbConn(rc.GinCtx)
2023-11-26 21:21:35 +08:00
res, err := dbi.GetDialect().GetColumns(tn)
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
rc.ResData = res
2021-01-08 15:37:32 +08:00
}
// @router /api/db/:dbId/hint-tables [get]
2023-01-14 16:29:52 +08:00
func (d *Db) HintTables(rc *req.Ctx) {
dbi := d.getDbConn(rc.GinCtx)
2023-11-26 21:21:35 +08:00
dm := dbi.GetDialect()
2022-08-10 19:46:17 +08:00
// 获取所有表
tables, err := dm.GetTables()
biz.ErrIsNil(err)
tableNames := make([]string, 0)
2021-04-16 15:10:07 +08:00
for _, v := range tables {
2023-05-24 12:32:17 +08:00
tableNames = append(tableNames, v.TableName)
}
// key = 表名value = 列名数组
res := make(map[string][]string)
// 表为空,则直接返回
if len(tableNames) == 0 {
rc.ResData = res
return
}
// 获取所有表下的所有列信息
columnMds, err := dm.GetColumns(tableNames...)
biz.ErrIsNil(err)
for _, v := range columnMds {
2023-05-24 12:32:17 +08:00
tName := v.TableName
if res[tName] == nil {
res[tName] = make([]string, 0)
2021-01-08 15:37:32 +08:00
}
2023-05-24 12:32:17 +08:00
columnName := fmt.Sprintf("%s [%s]", v.ColumnName, v.ColumnType)
comment := v.ColumnComment
// 如果字段备注不为空,则加上备注信息
2023-05-24 12:32:17 +08:00
if comment != "" {
columnName = fmt.Sprintf("%s[%s]", columnName, comment)
}
res[tName] = append(res[tName], columnName)
2021-04-16 15:10:07 +08:00
}
rc.ResData = res
2021-01-08 15:37:32 +08:00
}
2023-12-20 17:29:16 +08:00
func (d *Db) GetTableDDL(rc *req.Ctx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
2023-12-20 17:29:16 +08:00
res, err := d.getDbConn(rc.GinCtx).GetDialect().GetTableDDL(tn)
biz.ErrIsNilAppendErr(err, "获取表ddl失败: %s")
rc.ResData = res
}
2023-12-20 17:29:16 +08:00
func (d *Db) GetSchemas(rc *req.Ctx) {
2023-12-06 14:50:02 +08:00
res, err := d.getDbConn(rc.GinCtx).GetDialect().GetSchemas()
biz.ErrIsNilAppendErr(err, "获取schemas失败: %s")
rc.ResData = res
}
func getDbId(g *gin.Context) uint64 {
2021-04-16 15:10:07 +08:00
dbId, _ := strconv.Atoi(g.Param("dbId"))
2021-03-24 17:18:39 +08:00
biz.IsTrue(dbId > 0, "dbId错误")
2021-01-08 15:37:32 +08:00
return uint64(dbId)
}
func getDbName(g *gin.Context) string {
db := g.Query("db")
biz.NotEmpty(db, "db不能为空")
return db
}
func (d *Db) getDbConn(g *gin.Context) *dbm.DbConn {
dc, err := d.DbApp.GetDbConn(getDbId(g), getDbName(g))
biz.ErrIsNil(err)
return dc
}
2023-12-27 22:59:20 +08:00
// GetRestoreTask 获取数据库备份任务
// @router /api/instances/:instance/restore-task [GET]
func (d *Db) GetRestoreTask(rc *req.Ctx) {
// todo get restore task
panic("implement me")
}
// SaveRestoreTask 设置数据库备份任务
// @router /api/instances/:instance/restore-task [POST]
func (d *Db) SaveRestoreTask(rc *req.Ctx) {
// todo set restore task
panic("implement me")
}
// GetRestoreHistories 获取数据库备份历史
// @router /api/instances/:instance/restore-histories [GET]
func (d *Db) GetRestoreHistories(rc *req.Ctx) {
// todo get restore histories
panic("implement me")
}