mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
重构数据库备份与恢复模块 (#80)
* fix: 保存 LastResult 时截断字符串过长部分,以避免数据库报错 * refactor: 新增 entity.DbTaskBase 和 persistence.dbTaskBase, 用于实现数据库备份和恢复任务处理相关部分 * fix: aeskey变更后,解密密码出现数组越界访问错误 * fix: 时间属性为零值时,保存到 mysql 数据库报错 * refactor db.infrastructure.service.scheduler * feat: 实现立即备份功能 * refactor db.infrastructure.service.db_instance * refactor: 从数据库中获取数据库备份目录、mysql文件路径等配置信息 * fix: 数据库备份和恢复问题 * fix: 修改 .gitignore 文件,忽略数据库备份目录和数据库程序目录
This commit is contained in:
@@ -24,7 +24,7 @@ export function dateStrFormat(fmt: string, dateStr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function dateFormat(dateStr: string) {
|
export function dateFormat(dateStr: string) {
|
||||||
if (dateStr?.startsWith('0001-01-01', 0)) {
|
if (!dateStr) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
|
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
|
||||||
|
|||||||
@@ -24,9 +24,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
|
<div style="text-align: left">
|
||||||
<el-button @click="enableDbBackup(data)" v-if="!data.enabled" type="success" link>启用</el-button>
|
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
|
||||||
<el-button @click="disableDbBackup(data)" v-if="data.enabled" type="warning" link>禁用</el-button>
|
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
||||||
|
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||||
|
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -150,5 +153,20 @@ const disableDbBackup = async (data: any) => {
|
|||||||
await search();
|
await search();
|
||||||
ElMessage.success('禁用成功');
|
ElMessage.success('禁用成功');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startDbBackup = async (data: any) => {
|
||||||
|
let backupId: String;
|
||||||
|
if (data) {
|
||||||
|
backupId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要启用的备份任务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||||
|
await search();
|
||||||
|
ElMessage.success('备份任务启动成功');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -199,10 +199,10 @@ const init = async (data: any) => {
|
|||||||
state.form.dbBackupId = data.dbBackupId;
|
state.form.dbBackupId = data.dbBackupId;
|
||||||
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
|
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
|
||||||
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
|
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
|
||||||
if (data.dbBackupHistoryId > 0) {
|
if (data.pointInTime) {
|
||||||
state.restoreMode = 'backup-history';
|
|
||||||
} else {
|
|
||||||
state.restoreMode = 'point-in-time';
|
state.restoreMode = 'point-in-time';
|
||||||
|
} else {
|
||||||
|
state.restoreMode = 'backup-history';
|
||||||
}
|
}
|
||||||
state.history = {
|
state.history = {
|
||||||
dbBackupId: data.dbBackupId,
|
dbBackupId: data.dbBackupId,
|
||||||
@@ -232,35 +232,33 @@ const getDbNamesWithoutRestore = async () => {
|
|||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
restoreForm.value.validate(async (valid: any) => {
|
restoreForm.value.validate(async (valid: any) => {
|
||||||
if (!valid) {
|
if (valid) {
|
||||||
|
if (state.restoreMode == 'point-in-time') {
|
||||||
|
state.form.dbBackupId = 0;
|
||||||
|
state.form.dbBackupHistoryId = 0;
|
||||||
|
state.form.dbBackupHistoryName = '';
|
||||||
|
} else {
|
||||||
|
state.form.pointInTime = null;
|
||||||
|
}
|
||||||
|
state.form.repeated = false;
|
||||||
|
const reqForm = { ...state.form };
|
||||||
|
let api = dbApi.createDbRestore;
|
||||||
|
if (props.data) {
|
||||||
|
api = dbApi.saveDbRestore;
|
||||||
|
}
|
||||||
|
api.request(reqForm).then(() => {
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
emit('val-change', state.form);
|
||||||
|
state.btnLoading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
state.btnLoading = false;
|
||||||
|
}, 1000);
|
||||||
|
cancel();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
ElMessage.error('请正确填写信息');
|
ElMessage.error('请正确填写信息');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.restoreMode == 'point-in-time') {
|
|
||||||
state.form.dbBackupId = 0;
|
|
||||||
state.form.dbBackupHistoryId = 0;
|
|
||||||
state.form.dbBackupHistoryName = '';
|
|
||||||
} else {
|
|
||||||
state.form.pointInTime = '0001-01-01T00:00:00Z';
|
|
||||||
}
|
|
||||||
|
|
||||||
state.form.repeated = false;
|
|
||||||
const reqForm = { ...state.form };
|
|
||||||
let api = dbApi.createDbRestore;
|
|
||||||
if (props.data) {
|
|
||||||
api = dbApi.saveDbRestore;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
state.btnLoading = true;
|
|
||||||
await api.request(reqForm);
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
} finally {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
||||||
<el-button @click="enableDbRestore(data)" type="primary" link>启用</el-button>
|
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
||||||
<el-button @click="disableDbRestore(data)" type="primary" link>禁用</el-button>
|
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@
|
|||||||
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
|
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="!infoDialog.data.dbBackupHistoryName" :span="1" label="恢复时间点">{{
|
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
||||||
dateFormat(infoDialog.data.pointInTime)
|
dateFormat(infoDialog.data.pointInTime)
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="infoDialog.data.dbBackupHistoryName" :span="1" label="数据库备份">{{
|
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
||||||
infoDialog.data.dbBackupHistoryName
|
infoDialog.data.dbBackupHistoryName
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const dbApi = {
|
|||||||
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||||
|
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
||||||
|
|
||||||
|
|||||||
11
server/.gitignore
vendored
11
server/.gitignore
vendored
@@ -1,4 +1,11 @@
|
|||||||
static/static
|
/static/static/
|
||||||
config.yml
|
config.yml
|
||||||
mayfly_rsa
|
mayfly_rsa
|
||||||
mayfly_rsa.pub
|
mayfly_rsa.pub
|
||||||
|
|
||||||
|
# 数据库备份目录
|
||||||
|
/db/backup/
|
||||||
|
# mysql 程序目录
|
||||||
|
/db/mysql/
|
||||||
|
# mariadb 程序目录
|
||||||
|
/db/mariadb/
|
||||||
@@ -107,7 +107,7 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
|||||||
if otpStatus == OtpStatusNoReg {
|
if otpStatus == OtpStatusNoReg {
|
||||||
update := &sysentity.Account{OtpSecret: otpSecret}
|
update := &sysentity.Account{OtpSecret: otpSecret}
|
||||||
update.Id = accountId
|
update.Id = accountId
|
||||||
update.OtpSecretEncrypt()
|
biz.ErrIsNil(update.OtpSecretEncrypt())
|
||||||
biz.ErrIsNil(a.AccountApp.Update(context.Background(), update))
|
biz.ErrIsNil(a.AccountApp.Update(context.Background(), update))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVerifyInfo, string, string) {
|
func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVerifyInfo, string, string) {
|
||||||
account.OtpSecretDecrypt()
|
biz.ErrIsNil(account.OtpSecretDecrypt())
|
||||||
otpSecret := account.OtpSecret
|
otpSecret := account.OtpSecret
|
||||||
// 修改状态为已注册
|
// 修改状态为已注册
|
||||||
otpStatus := OtpStatusReg
|
otpStatus := OtpStatusReg
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mayfly-go/pkg/biz"
|
|
||||||
"mayfly-go/pkg/config"
|
"mayfly-go/pkg/config"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
@@ -27,30 +26,34 @@ func CheckAccountPasswordLever(ps string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用config.yml的aes.key进行密码加密
|
// 使用config.yml的aes.key进行密码加密
|
||||||
func PwdAesEncrypt(password string) string {
|
func PwdAesEncrypt(password string) (string, error) {
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return ""
|
return "", nil
|
||||||
}
|
}
|
||||||
aes := config.Conf.Aes
|
aes := config.Conf.Aes
|
||||||
if aes.Key == "" {
|
if aes.Key == "" {
|
||||||
return password
|
return password, nil
|
||||||
}
|
}
|
||||||
encryptPwd, err := aes.EncryptBase64([]byte(password))
|
encryptPwd, err := aes.EncryptBase64([]byte(password))
|
||||||
biz.ErrIsNilAppendErr(err, "密码加密失败: %s")
|
if err != nil {
|
||||||
return encryptPwd
|
return "", err
|
||||||
|
}
|
||||||
|
return encryptPwd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用config.yml的aes.key进行密码解密
|
// 使用config.yml的aes.key进行密码解密
|
||||||
func PwdAesDecrypt(encryptPwd string) string {
|
func PwdAesDecrypt(encryptPwd string) (string, error) {
|
||||||
if encryptPwd == "" {
|
if encryptPwd == "" {
|
||||||
return ""
|
return "", nil
|
||||||
}
|
}
|
||||||
aes := config.Conf.Aes
|
aes := config.Conf.Aes
|
||||||
if aes.Key == "" {
|
if aes.Key == "" {
|
||||||
return encryptPwd
|
return encryptPwd, nil
|
||||||
}
|
}
|
||||||
decryptPwd, err := aes.DecryptBase64(encryptPwd)
|
decryptPwd, err := aes.DecryptBase64(encryptPwd)
|
||||||
biz.ErrIsNilAppendErr(err, "密码解密失败: %s")
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
// 解密后的密码
|
// 解密后的密码
|
||||||
return string(decryptPwd)
|
return string(decryptPwd), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
|||||||
// Create 保存数据库备份任务
|
// Create 保存数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups [POST]
|
// @router /api/dbs/:dbId/backups [POST]
|
||||||
func (d *DbBackup) Create(rc *req.Ctx) {
|
func (d *DbBackup) Create(rc *req.Ctx) {
|
||||||
form := &form.DbBackupForm{}
|
backupForm := &form.DbBackupForm{}
|
||||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||||
rc.ReqParam = form
|
rc.ReqParam = backupForm
|
||||||
|
|
||||||
dbNames := strings.Fields(form.DbNames)
|
dbNames := strings.Fields(backupForm.DbNames)
|
||||||
biz.IsTrue(len(dbNames) > 0, "解析数据库备份任务失败:数据库名称未定义")
|
biz.IsTrue(len(dbNames) > 0, "解析数据库备份任务失败:数据库名称未定义")
|
||||||
|
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
@@ -54,14 +54,10 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
|||||||
tasks := make([]*entity.DbBackup, 0, len(dbNames))
|
tasks := make([]*entity.DbBackup, 0, len(dbNames))
|
||||||
for _, dbName := range dbNames {
|
for _, dbName := range dbNames {
|
||||||
task := &entity.DbBackup{
|
task := &entity.DbBackup{
|
||||||
|
DbTaskBase: entity.NewDbBTaskBase(true, backupForm.Repeated, backupForm.StartTime, backupForm.Interval),
|
||||||
DbName: dbName,
|
DbName: dbName,
|
||||||
Name: form.Name,
|
Name: backupForm.Name,
|
||||||
StartTime: form.StartTime,
|
|
||||||
Interval: form.Interval,
|
|
||||||
Enabled: true,
|
|
||||||
Repeated: form.Repeated,
|
|
||||||
DbInstanceId: db.InstanceId,
|
DbInstanceId: db.InstanceId,
|
||||||
LastTime: form.StartTime,
|
|
||||||
}
|
}
|
||||||
tasks = append(tasks, task)
|
tasks = append(tasks, task)
|
||||||
}
|
}
|
||||||
@@ -71,17 +67,15 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
|||||||
// Save 保存数据库备份任务
|
// Save 保存数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups/:backupId [PUT]
|
// @router /api/dbs/:dbId/backups/:backupId [PUT]
|
||||||
func (d *DbBackup) Save(rc *req.Ctx) {
|
func (d *DbBackup) Save(rc *req.Ctx) {
|
||||||
form := &form.DbBackupForm{}
|
backupForm := &form.DbBackupForm{}
|
||||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||||
rc.ReqParam = form
|
rc.ReqParam = backupForm
|
||||||
|
|
||||||
task := &entity.DbBackup{
|
task := &entity.DbBackup{}
|
||||||
Name: form.Name,
|
task.Id = backupForm.Id
|
||||||
StartTime: form.StartTime,
|
task.Name = backupForm.Name
|
||||||
Interval: form.Interval,
|
task.StartTime = backupForm.StartTime
|
||||||
LastTime: form.StartTime,
|
task.Interval = backupForm.Interval
|
||||||
}
|
|
||||||
task.Id = form.Id
|
|
||||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Save(rc.MetaCtx, task), "保存数据库备份任务失败: %v")
|
biz.ErrIsNilAppendErr(d.DbBackupApp.Save(rc.MetaCtx, task), "保存数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +119,13 @@ func (d *DbBackup) Disable(rc *req.Ctx) {
|
|||||||
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start 禁用数据库备份任务
|
||||||
|
// @router /api/dbs/:dbId/backups/:taskId/start [PUT]
|
||||||
|
func (d *DbBackup) Start(rc *req.Ctx) {
|
||||||
|
err := d.walk(rc, d.DbBackupApp.Start)
|
||||||
|
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||||
|
}
|
||||||
|
|
||||||
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
||||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
|||||||
// Create 保存数据库恢复任务
|
// Create 保存数据库恢复任务
|
||||||
// @router /api/dbs/:dbId/restores [POST]
|
// @router /api/dbs/:dbId/restores [POST]
|
||||||
func (d *DbRestore) Create(rc *req.Ctx) {
|
func (d *DbRestore) Create(rc *req.Ctx) {
|
||||||
form := &form.DbRestoreForm{}
|
restoreForm := &form.DbRestoreForm{}
|
||||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
ginx.BindJsonAndValid(rc.GinCtx, restoreForm)
|
||||||
rc.ReqParam = form
|
rc.ReqParam = restoreForm
|
||||||
|
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||||
@@ -48,16 +48,13 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
|||||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
|
|
||||||
task := &entity.DbRestore{
|
task := &entity.DbRestore{
|
||||||
DbName: form.DbName,
|
DbTaskBase: entity.NewDbBTaskBase(true, restoreForm.Repeated, restoreForm.StartTime, restoreForm.Interval),
|
||||||
StartTime: form.StartTime,
|
DbName: restoreForm.DbName,
|
||||||
Interval: form.Interval,
|
|
||||||
Enabled: true,
|
|
||||||
Repeated: form.Repeated,
|
|
||||||
DbInstanceId: db.InstanceId,
|
DbInstanceId: db.InstanceId,
|
||||||
PointInTime: form.PointInTime,
|
PointInTime: restoreForm.PointInTime,
|
||||||
DbBackupId: form.DbBackupId,
|
DbBackupId: restoreForm.DbBackupId,
|
||||||
DbBackupHistoryId: form.DbBackupHistoryId,
|
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||||
DbBackupHistoryName: form.DbBackupHistoryName,
|
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||||
}
|
}
|
||||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, task), "添加数据库恢复任务失败: %v")
|
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, task), "添加数据库恢复任务失败: %v")
|
||||||
}
|
}
|
||||||
@@ -65,15 +62,14 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
|||||||
// Save 保存数据库恢复任务
|
// Save 保存数据库恢复任务
|
||||||
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
|
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
|
||||||
func (d *DbRestore) Save(rc *req.Ctx) {
|
func (d *DbRestore) Save(rc *req.Ctx) {
|
||||||
form := &form.DbRestoreForm{}
|
restoreForm := &form.DbRestoreForm{}
|
||||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
ginx.BindJsonAndValid(rc.GinCtx, restoreForm)
|
||||||
rc.ReqParam = form
|
rc.ReqParam = restoreForm
|
||||||
|
|
||||||
task := &entity.DbRestore{
|
task := &entity.DbRestore{}
|
||||||
StartTime: form.StartTime,
|
task.Id = restoreForm.Id
|
||||||
Interval: form.Interval,
|
task.StartTime = restoreForm.StartTime
|
||||||
}
|
task.Interval = restoreForm.Interval
|
||||||
task.Id = form.Id
|
|
||||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Save(rc.MetaCtx, task), "保存数据库恢复任务失败: %v")
|
biz.ErrIsNilAppendErr(d.DbRestoreApp.Save(rc.MetaCtx, task), "保存数据库恢复任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,22 @@ package form
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DbRestoreForm 数据库备份表单
|
// DbRestoreForm 数据库备份表单
|
||||||
type DbRestoreForm struct {
|
type DbRestoreForm struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
DbName string `binding:"required" json:"dbName"` // 数据库名
|
DbName string `binding:"required" json:"dbName"` // 数据库名
|
||||||
StartTime time.Time `binding:"required" json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
StartTime time.Time `binding:"required" json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
||||||
PointInTime time.Time `json:"PointInTime"` // 指定时间
|
PointInTime timex.NullTime `json:"pointInTime"` // 指定时间
|
||||||
DbBackupId uint64 `json:"dbBackupId"` // 数据库备份任务ID
|
DbBackupId uint64 `json:"dbBackupId"` // 数据库备份任务ID
|
||||||
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 数据库备份历史ID
|
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 数据库备份历史ID
|
||||||
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库备份历史名称
|
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库备份历史名称
|
||||||
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
||||||
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
||||||
Repeated bool `json:"repeated"` // 是否重复执行
|
Repeated bool `json:"repeated"` // 是否重复执行
|
||||||
}
|
}
|
||||||
|
|
||||||
func (restore *DbRestoreForm) UnmarshalJSON(data []byte) error {
|
func (restore *DbRestoreForm) UnmarshalJSON(data []byte) error {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (d *Instance) GetInstancePwd(rc *req.Ctx) {
|
|||||||
instanceId := getInstanceId(rc.GinCtx)
|
instanceId := getInstanceId(rc.GinCtx)
|
||||||
instanceEntity, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
|
instanceEntity, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
|
||||||
biz.ErrIsNil(err, "获取数据库实例错误")
|
biz.ErrIsNil(err, "获取数据库实例错误")
|
||||||
instanceEntity.PwdDecrypt()
|
biz.ErrIsNil(instanceEntity.PwdDecrypt())
|
||||||
rc.ResData = instanceEntity.Password
|
rc.ResData = instanceEntity.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
|
|||||||
instanceId := getInstanceId(rc.GinCtx)
|
instanceId := getInstanceId(rc.GinCtx)
|
||||||
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
|
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
|
||||||
biz.ErrIsNil(err, "获取数据库实例错误")
|
biz.ErrIsNil(err, "获取数据库实例错误")
|
||||||
instance.PwdDecrypt()
|
biz.ErrIsNil(instance.PwdDecrypt())
|
||||||
res, err := d.InstanceApp.GetDatabases(instance)
|
res, err := d.InstanceApp.GetDatabases(instance)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
|
|||||||
@@ -2,27 +2,28 @@ package vo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DbBackupHistory 数据库备份任务
|
// DbBackup 数据库备份任务
|
||||||
type DbBackup struct {
|
type DbBackup struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
DbName string `json:"dbName"` // 数据库名
|
DbName string `json:"dbName"` // 数据库名
|
||||||
CreateTime time.Time `json:"createTime"` // 创建时间: 2023-11-08 02:00:00
|
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||||
StartTime time.Time `json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
StartTime time.Time `json:"startTime"` // 开始时间
|
||||||
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
Interval time.Duration `json:"-"` // 间隔时间
|
||||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
LastTime time.Time `json:"lastTime"` // 最近一次执行时间: 2023-11-08 02:00:00
|
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
Name string `json:"name"` // 备份任务名称
|
Name string `json:"name"` // 备份任务名称
|
||||||
}
|
}
|
||||||
|
|
||||||
func (restore *DbBackup) MarshalJSON() ([]byte, error) {
|
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||||
type dbBackup DbBackup
|
type dbBackup DbBackup
|
||||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||||
return json.Marshal((*dbBackup)(restore))
|
return json.Marshal((*dbBackup)(backup))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,26 @@ package vo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DbRestore 数据库备份任务
|
// DbRestore 数据库备份任务
|
||||||
type DbRestore struct {
|
type DbRestore struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
DbName string `json:"dbName"` // 数据库名
|
DbName string `json:"dbName"` // 数据库名
|
||||||
StartTime time.Time `json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
StartTime time.Time `json:"startTime"` // 开始时间
|
||||||
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
Interval time.Duration `json:"-"` // 间隔时间
|
||||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
LastTime time.Time `json:"lastTime"` // 最近一次执行时间: 2023-11-08 02:00:00
|
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||||
PointInTime time.Time `json:"pointInTime"` // 指定数据库恢复的时间点
|
PointInTime timex.NullTime `json:"pointInTime"` // 指定数据库恢复的时间点
|
||||||
DbBackupId uint64 `json:"dbBackupId"` // 数据库备份任务ID
|
DbBackupId uint64 `json:"dbBackupId"` // 数据库备份任务ID
|
||||||
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 数据库备份历史ID
|
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 数据库备份历史ID
|
||||||
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库备份历史名称
|
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库备份历史名称
|
||||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var (
|
|||||||
dbBackupHistoryApp *DbBackupHistoryApp
|
dbBackupHistoryApp *DbBackupHistoryApp
|
||||||
dbRestoreApp *DbRestoreApp
|
dbRestoreApp *DbRestoreApp
|
||||||
dbRestoreHistoryApp *DbRestoreHistoryApp
|
dbRestoreHistoryApp *DbRestoreHistoryApp
|
||||||
|
dbBinlogApp *DbBinlogApp
|
||||||
)
|
)
|
||||||
|
|
||||||
var repositories *repository.Repositories
|
var repositories *repository.Repositories
|
||||||
@@ -39,16 +40,26 @@ func Init() {
|
|||||||
dbSqlExecApp = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
|
dbSqlExecApp = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
|
||||||
dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
|
dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
|
||||||
|
|
||||||
dbBackupApp, err = newDbBackupApp(repositories)
|
dbBackupApp, err = newDbBackupApp(repositories, dbApp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
||||||
}
|
}
|
||||||
dbRestoreApp, err = newDbRestoreApp(repositories)
|
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
|
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
|
||||||
}
|
}
|
||||||
dbBackupHistoryApp, err = newDbBackupHistoryApp(repositories)
|
dbBackupHistoryApp, err = newDbBackupHistoryApp(repositories)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("初始化 dbBackupHistoryApp 失败: %v", err))
|
||||||
|
}
|
||||||
dbRestoreHistoryApp, err = newDbRestoreHistoryApp(repositories)
|
dbRestoreHistoryApp, err = newDbRestoreHistoryApp(repositories)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("初始化 dbRestoreHistoryApp 失败: %v", err))
|
||||||
|
}
|
||||||
|
dbBinlogApp, err = newDbBinlogApp(repositories, dbApp)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +94,7 @@ func GetDbRestoreApp() *DbRestoreApp {
|
|||||||
func GetDbRestoreHistoryApp() *DbRestoreHistoryApp {
|
func GetDbRestoreHistoryApp() *DbRestoreHistoryApp {
|
||||||
return dbRestoreHistoryApp
|
return dbRestoreHistoryApp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDbBinlogApp() *DbBinlogApp {
|
||||||
|
return dbBinlogApp
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) {
|
|||||||
|
|
||||||
checkDb := dbName
|
checkDb := dbName
|
||||||
// 兼容pgsql/dm db/schema模式
|
// 兼容pgsql/dm db/schema模式
|
||||||
if instance.Type == dbm.DbTypePostgres || instance.Type == dbm.DM {
|
if dbm.DbTypePostgres.Equal(instance.Type) || dbm.DM.Equal(instance.Type) {
|
||||||
ss := strings.Split(dbName, "/")
|
ss := strings.Split(dbName, "/")
|
||||||
if len(ss) > 1 {
|
if len(ss) > 1 {
|
||||||
checkDb = ss[0]
|
checkDb = ss[0]
|
||||||
@@ -167,7 +167,9 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 密码解密
|
// 密码解密
|
||||||
instance.PwdDecrypt()
|
if err := instance.PwdDecrypt(); err != nil {
|
||||||
|
return nil, errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
return toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), nil
|
return toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,62 +2,155 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
service2 "mayfly-go/internal/db/infrastructure/service"
|
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDbBackupApp(repositories *repository.Repositories) (*DbBackupApp, error) {
|
func newDbBackupApp(repositories *repository.Repositories, dbApp Db) (*DbBackupApp, error) {
|
||||||
binlogSvc, err := service2.NewDbBinlogSvc(repositories)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dbBackupSvc, err := service2.NewDbBackupSvc(repositories, binlogSvc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
app := &DbBackupApp{
|
app := &DbBackupApp{
|
||||||
repo: repositories.Backup,
|
backupRepo: repositories.Backup,
|
||||||
dbBackupSvc: dbBackupSvc,
|
instanceRepo: repositories.Instance,
|
||||||
|
backupHistoryRepo: repositories.BackupHistory,
|
||||||
|
dbApp: dbApp,
|
||||||
}
|
}
|
||||||
|
scheduler, err := newDbScheduler[*entity.DbBackup](
|
||||||
|
repositories.Backup,
|
||||||
|
withRunBackupTask(app))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.scheduler = scheduler
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DbBackupApp struct {
|
type DbBackupApp struct {
|
||||||
repo repository.DbBackup
|
backupRepo repository.DbBackup
|
||||||
dbBackupSvc service.DbBackupSvc
|
instanceRepo repository.Instance
|
||||||
|
backupHistoryRepo repository.DbBackupHistory
|
||||||
|
dbApp Db
|
||||||
|
scheduler *dbScheduler[*entity.DbBackup]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBackupApp) Close() {
|
||||||
|
app.scheduler.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Create(ctx context.Context, tasks ...*entity.DbBackup) error {
|
func (app *DbBackupApp) Create(ctx context.Context, tasks ...*entity.DbBackup) error {
|
||||||
return app.dbBackupSvc.AddTask(ctx, tasks...)
|
return app.scheduler.AddTask(ctx, tasks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Save(ctx context.Context, task *entity.DbBackup) error {
|
func (app *DbBackupApp) Save(ctx context.Context, task *entity.DbBackup) error {
|
||||||
return app.dbBackupSvc.UpdateTask(ctx, task)
|
return app.scheduler.UpdateTask(ctx, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Delete(ctx context.Context, taskId uint64) error {
|
func (app *DbBackupApp) Delete(ctx context.Context, taskId uint64) error {
|
||||||
// todo: 删除数据库备份历史文件
|
// todo: 删除数据库备份历史文件
|
||||||
return app.dbBackupSvc.DeleteTask(ctx, taskId)
|
return app.scheduler.DeleteTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Enable(ctx context.Context, taskId uint64) error {
|
func (app *DbBackupApp) Enable(ctx context.Context, taskId uint64) error {
|
||||||
return app.dbBackupSvc.EnableTask(ctx, taskId)
|
return app.scheduler.EnableTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Disable(ctx context.Context, taskId uint64) error {
|
func (app *DbBackupApp) Disable(ctx context.Context, taskId uint64) error {
|
||||||
return app.dbBackupSvc.DisableTask(ctx, taskId)
|
return app.scheduler.DisableTask(ctx, taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBackupApp) Start(ctx context.Context, taskId uint64) error {
|
||||||
|
return app.scheduler.StartTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPageList 分页获取数据库备份任务
|
// GetPageList 分页获取数据库备份任务
|
||||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||||
return app.repo.GetDbBackupList(condition, pageParam, toEntity, orderBy...)
|
return app.backupRepo.GetDbBackupList(condition, pageParam, toEntity, orderBy...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
||||||
func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
|
func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
|
||||||
return app.repo.GetDbNamesWithoutBackup(instanceId, dbNames)
|
return app.backupRepo.GetDbNamesWithoutBackup(instanceId, dbNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withRunBackupTask(app *DbBackupApp) dbSchedulerOption[*entity.DbBackup] {
|
||||||
|
return func(scheduler *dbScheduler[*entity.DbBackup]) {
|
||||||
|
scheduler.RunTask = app.runTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBackupApp) runTask(ctx context.Context, task *entity.DbBackup) error {
|
||||||
|
id, err := NewIncUUID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
history := &entity.DbBackupHistory{
|
||||||
|
Uuid: id.String(),
|
||||||
|
DbBackupId: task.Id,
|
||||||
|
DbInstanceId: task.DbInstanceId,
|
||||||
|
DbName: task.DbName,
|
||||||
|
}
|
||||||
|
conn, err := app.dbApp.GetDbConnByInstanceId(task.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbProgram := conn.GetDialect().GetDbProgram()
|
||||||
|
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
name := task.Name
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = task.DbName
|
||||||
|
}
|
||||||
|
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||||
|
history.CreateTime = now
|
||||||
|
history.BinlogFileName = binlogInfo.FileName
|
||||||
|
history.BinlogSequence = binlogInfo.Sequence
|
||||||
|
history.BinlogPosition = binlogInfo.Position
|
||||||
|
|
||||||
|
if err := app.backupHistoryRepo.Insert(ctx, history); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIncUUID() (uuid.UUID, error) {
|
||||||
|
var uid uuid.UUID
|
||||||
|
now, seq, err := uuid.GetTime()
|
||||||
|
if err != nil {
|
||||||
|
return uid, err
|
||||||
|
}
|
||||||
|
timeHi := uint32((now >> 28) & 0xffffffff)
|
||||||
|
timeMid := uint16((now >> 12) & 0xffff)
|
||||||
|
timeLow := uint16(now & 0x0fff)
|
||||||
|
timeLow |= 0x1000 // Version 1
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(uid[0:], timeHi)
|
||||||
|
binary.BigEndian.PutUint16(uid[4:], timeMid)
|
||||||
|
binary.BigEndian.PutUint16(uid[6:], timeLow)
|
||||||
|
binary.BigEndian.PutUint16(uid[8:], seq)
|
||||||
|
|
||||||
|
copy(uid[10:], uuid.NodeID())
|
||||||
|
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDbBackupHistoryApp(repositories *repository.Repositories) (*DbBackupHistoryApp, error) {
|
||||||
|
app := &DbBackupHistoryApp{
|
||||||
|
repo: repositories.BackupHistory,
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbBackupHistoryApp struct {
|
||||||
|
repo repository.DbBackupHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPageList 分页获取数据库备份历史
|
||||||
|
func (app *DbBackupHistoryApp) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||||
|
return app.repo.GetHistories(condition, pageParam, toEntity, orderBy...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newDbBackupHistoryApp(repositories *repository.Repositories) (*DbBackupHistoryApp, error) {
|
|
||||||
app := &DbBackupHistoryApp{
|
|
||||||
repo: repositories.BackupHistory,
|
|
||||||
}
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DbBackupHistoryApp struct {
|
|
||||||
repo repository.DbBackupHistory
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageList 分页获取数据库备份历史
|
|
||||||
func (app *DbBackupHistoryApp) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
|
||||||
return app.repo.GetHistories(condition, pageParam, toEntity, orderBy...)
|
|
||||||
}
|
|
||||||
154
server/internal/db/application/db_binlog.go
Normal file
154
server/internal/db/application/db_binlog.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/internal/db/domain/entity"
|
||||||
|
"mayfly-go/internal/db/domain/repository"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/utils/stringx"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
binlogDownloadInterval = time.Minute * 15
|
||||||
|
)
|
||||||
|
|
||||||
|
type DbBinlogApp struct {
|
||||||
|
binlogRepo repository.DbBinlog
|
||||||
|
binlogHistoryRepo repository.DbBinlogHistory
|
||||||
|
backupRepo repository.DbBackup
|
||||||
|
backupHistoryRepo repository.DbBackupHistory
|
||||||
|
dbApp Db
|
||||||
|
context context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
waitGroup sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
binlogResult = map[entity.TaskStatus]string{
|
||||||
|
entity.TaskDelay: "等待备份BINLOG",
|
||||||
|
entity.TaskReady: "准备备份BINLOG",
|
||||||
|
entity.TaskReserved: "BINLOG备份中",
|
||||||
|
entity.TaskSuccess: "BINLOG备份成功",
|
||||||
|
entity.TaskFailed: "BINLOG备份失败",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDbBinlogApp(repositories *repository.Repositories, dbApp Db) (*DbBinlogApp, error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
svc := &DbBinlogApp{
|
||||||
|
binlogRepo: repositories.Binlog,
|
||||||
|
binlogHistoryRepo: repositories.BinlogHistory,
|
||||||
|
backupRepo: repositories.Backup,
|
||||||
|
backupHistoryRepo: repositories.BackupHistory,
|
||||||
|
dbApp: dbApp,
|
||||||
|
context: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
svc.waitGroup.Add(1)
|
||||||
|
go svc.run()
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) runTask(ctx context.Context, backup *entity.DbBackup) error {
|
||||||
|
if err := app.AddTaskIfNotExists(ctx, entity.NewDbBinlog(backup.DbInstanceId)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||||
|
binlogHistory, ok, err := app.binlogHistoryRepo.GetLatestHistory(backup.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
latestBinlogSequence = binlogHistory.Sequence
|
||||||
|
} else {
|
||||||
|
backupHistory, err := app.backupHistoryRepo.GetEarliestHistory(backup.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
earliestBackupSequence = backupHistory.BinlogSequence
|
||||||
|
}
|
||||||
|
conn, err := app.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbProgram := conn.GetDialect().GetDbProgram()
|
||||||
|
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
|
||||||
|
if err == nil {
|
||||||
|
err = app.binlogHistoryRepo.InsertWithBinlogFiles(ctx, backup.DbInstanceId, binlogFiles)
|
||||||
|
}
|
||||||
|
taskStatus := entity.TaskSuccess
|
||||||
|
if err != nil {
|
||||||
|
taskStatus = entity.TaskFailed
|
||||||
|
}
|
||||||
|
task := &entity.DbBinlog{}
|
||||||
|
task.Id = backup.DbInstanceId
|
||||||
|
return app.updateCurTask(ctx, taskStatus, err, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) run() {
|
||||||
|
defer app.waitGroup.Done()
|
||||||
|
|
||||||
|
for !app.closed() {
|
||||||
|
app.fetchFromAllInstances()
|
||||||
|
timex.SleepWithContext(app.context, binlogDownloadInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) fetchFromAllInstances() {
|
||||||
|
tasks, err := app.backupRepo.ListRepeating()
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 获取数据库备份任务失败: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if app.closed() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := app.runTask(app.context, task); err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 下载 binlog 文件失败: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) Close() {
|
||||||
|
app.cancel()
|
||||||
|
app.waitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) closed() bool {
|
||||||
|
return app.context.Err() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error {
|
||||||
|
if err := app.binlogRepo.AddTaskIfNotExists(ctx, task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if task.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) DeleteTask(ctx context.Context, taskId uint64) error {
|
||||||
|
// todo: 删除 Binlog 历史文件
|
||||||
|
if err := app.binlogRepo.DeleteById(ctx, taskId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) updateCurTask(ctx context.Context, status entity.TaskStatus, lastErr error, task *entity.DbBinlog) error {
|
||||||
|
task.LastStatus = status
|
||||||
|
var result = binlogResult[status]
|
||||||
|
if lastErr != nil {
|
||||||
|
result = fmt.Sprintf("%v: %v", binlogResult[status], lastErr)
|
||||||
|
}
|
||||||
|
task.LastResult = stringx.TruncateStr(result, entity.LastResultSize)
|
||||||
|
task.LastTime = timex.NewNullTime(time.Now())
|
||||||
|
return app.binlogRepo.UpdateById(ctx, task, "last_status", "last_result", "last_time")
|
||||||
|
}
|
||||||
@@ -2,57 +2,190 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"mayfly-go/internal/db/dbm"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
serviceImpl "mayfly-go/internal/db/infrastructure/service"
|
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDbRestoreApp(repositories *repository.Repositories) (*DbRestoreApp, error) {
|
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db) (*DbRestoreApp, error) {
|
||||||
dbRestoreSvc, err := serviceImpl.NewDbRestoreSvc(repositories)
|
app := &DbRestoreApp{
|
||||||
|
restoreRepo: repositories.Restore,
|
||||||
|
instanceRepo: repositories.Instance,
|
||||||
|
backupHistoryRepo: repositories.BackupHistory,
|
||||||
|
restoreHistoryRepo: repositories.RestoreHistory,
|
||||||
|
binlogHistoryRepo: repositories.BinlogHistory,
|
||||||
|
dbApp: dbApp,
|
||||||
|
}
|
||||||
|
scheduler, err := newDbScheduler[*entity.DbRestore](
|
||||||
|
repositories.Restore,
|
||||||
|
withRunRestoreTask(app))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
app := &DbRestoreApp{
|
app.scheduler = scheduler
|
||||||
repo: repositories.Restore,
|
|
||||||
dbRestoreSvc: dbRestoreSvc,
|
|
||||||
}
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DbRestoreApp struct {
|
type DbRestoreApp struct {
|
||||||
repo repository.DbRestore
|
restoreRepo repository.DbRestore
|
||||||
dbRestoreSvc service.DbRestoreSvc
|
instanceRepo repository.Instance
|
||||||
|
backupHistoryRepo repository.DbBackupHistory
|
||||||
|
restoreHistoryRepo repository.DbRestoreHistory
|
||||||
|
binlogHistoryRepo repository.DbBinlogHistory
|
||||||
|
dbApp Db
|
||||||
|
scheduler *dbScheduler[*entity.DbRestore]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbRestoreApp) Close() {
|
||||||
|
app.scheduler.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Create(ctx context.Context, tasks ...*entity.DbRestore) error {
|
func (app *DbRestoreApp) Create(ctx context.Context, tasks ...*entity.DbRestore) error {
|
||||||
return app.dbRestoreSvc.AddTask(ctx, tasks...)
|
return app.scheduler.AddTask(ctx, tasks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Save(ctx context.Context, task *entity.DbRestore) error {
|
func (app *DbRestoreApp) Save(ctx context.Context, task *entity.DbRestore) error {
|
||||||
return app.dbRestoreSvc.UpdateTask(ctx, task)
|
return app.scheduler.UpdateTask(ctx, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Delete(ctx context.Context, taskId uint64) error {
|
func (app *DbRestoreApp) Delete(ctx context.Context, taskId uint64) error {
|
||||||
// todo: 删除数据库恢复历史文件
|
// todo: 删除数据库恢复历史文件
|
||||||
return app.dbRestoreSvc.DeleteTask(ctx, taskId)
|
return app.scheduler.DeleteTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Enable(ctx context.Context, taskId uint64) error {
|
func (app *DbRestoreApp) Enable(ctx context.Context, taskId uint64) error {
|
||||||
return app.dbRestoreSvc.EnableTask(ctx, taskId)
|
return app.scheduler.EnableTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Disable(ctx context.Context, taskId uint64) error {
|
func (app *DbRestoreApp) Disable(ctx context.Context, taskId uint64) error {
|
||||||
return app.dbRestoreSvc.DisableTask(ctx, taskId)
|
return app.scheduler.DisableTask(ctx, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPageList 分页获取数据库恢复任务
|
// GetPageList 分页获取数据库恢复任务
|
||||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||||
return app.repo.GetDbRestoreList(condition, pageParam, toEntity, orderBy...)
|
return app.restoreRepo.GetDbRestoreList(condition, pageParam, toEntity, orderBy...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||||
return app.repo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbRestoreApp) runTask(ctx context.Context, task *entity.DbRestore) error {
|
||||||
|
conn, err := app.dbApp.GetDbConnByInstanceId(task.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbProgram := conn.GetDialect().GetDbProgram()
|
||||||
|
if task.PointInTime.Valid {
|
||||||
|
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||||
|
binlogHistory, ok, err := app.binlogHistoryRepo.GetLatestHistory(task.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
latestBinlogSequence = binlogHistory.Sequence
|
||||||
|
} else {
|
||||||
|
backupHistory, err := app.backupHistoryRepo.GetEarliestHistory(task.DbInstanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
earliestBackupSequence = backupHistory.BinlogSequence
|
||||||
|
}
|
||||||
|
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := app.binlogHistoryRepo.InsertWithBinlogFiles(ctx, task.DbInstanceId, binlogFiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := app.restorePointInTime(ctx, dbProgram, task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := app.restoreBackupHistory(ctx, dbProgram, task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history := &entity.DbRestoreHistory{
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
DbRestoreId: task.Id,
|
||||||
|
}
|
||||||
|
if err := app.restoreHistoryRepo.Insert(ctx, history); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbRestoreApp) restorePointInTime(ctx context.Context, program dbm.DbProgram, task *entity.DbRestore) error {
|
||||||
|
binlogHistory, err := app.binlogHistoryRepo.GetHistoryByTime(task.DbInstanceId, task.PointInTime.Time)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, task.PointInTime.Time)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := &entity.BinlogInfo{
|
||||||
|
FileName: binlogHistory.FileName,
|
||||||
|
Sequence: binlogHistory.Sequence,
|
||||||
|
Position: position,
|
||||||
|
}
|
||||||
|
backupHistory, err := app.backupHistoryRepo.GetLatestHistory(task.DbInstanceId, task.DbName, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := &entity.BinlogInfo{
|
||||||
|
FileName: backupHistory.BinlogFileName,
|
||||||
|
Sequence: backupHistory.BinlogSequence,
|
||||||
|
Position: backupHistory.BinlogPosition,
|
||||||
|
}
|
||||||
|
binlogHistories, err := app.binlogHistoryRepo.GetHistories(task.DbInstanceId, start, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
restoreInfo := &dbm.RestoreInfo{
|
||||||
|
BackupHistory: backupHistory,
|
||||||
|
BinlogHistories: binlogHistories,
|
||||||
|
StartPosition: backupHistory.BinlogPosition,
|
||||||
|
TargetPosition: target.Position,
|
||||||
|
TargetTime: task.PointInTime.Time,
|
||||||
|
}
|
||||||
|
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return program.ReplayBinlog(ctx, task.DbName, task.DbName, restoreInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbRestoreApp) restoreBackupHistory(ctx context.Context, program dbm.DbProgram, task *entity.DbRestore) error {
|
||||||
|
backupHistory := &entity.DbBackupHistory{}
|
||||||
|
if err := app.backupHistoryRepo.GetById(backupHistory, task.DbBackupHistoryId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withRunRestoreTask(app *DbRestoreApp) dbSchedulerOption[*entity.DbRestore] {
|
||||||
|
return func(scheduler *dbScheduler[*entity.DbRestore]) {
|
||||||
|
scheduler.RunTask = app.runTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDbRestoreHistoryApp(repositories *repository.Repositories) (*DbRestoreHistoryApp, error) {
|
||||||
|
app := &DbRestoreHistoryApp{
|
||||||
|
repo: repositories.RestoreHistory,
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbRestoreHistoryApp struct {
|
||||||
|
repo repository.DbRestoreHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPageList 分页获取数据库备份历史
|
||||||
|
func (app *DbRestoreHistoryApp) GetPageList(condition *entity.DbRestoreHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||||
|
return app.repo.GetDbRestoreHistories(condition, pageParam, toEntity, orderBy...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newDbRestoreHistoryApp(repositories *repository.Repositories) (*DbRestoreHistoryApp, error) {
|
|
||||||
app := &DbRestoreHistoryApp{
|
|
||||||
repo: repositories.RestoreHistory,
|
|
||||||
}
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DbRestoreHistoryApp struct {
|
|
||||||
repo repository.DbRestoreHistory
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageList 分页获取数据库备份历史
|
|
||||||
func (app *DbRestoreHistoryApp) GetPageList(condition *entity.DbRestoreHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
|
||||||
return app.repo.GetDbRestoreHistories(condition, pageParam, toEntity, orderBy...)
|
|
||||||
}
|
|
||||||
235
server/internal/db/application/db_scheduler.go
Normal file
235
server/internal/db/application/db_scheduler.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/internal/db/domain/entity"
|
||||||
|
"mayfly-go/internal/db/domain/repository"
|
||||||
|
"mayfly-go/pkg/queue"
|
||||||
|
"mayfly-go/pkg/utils/anyx"
|
||||||
|
"mayfly-go/pkg/utils/stringx"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sleepAfterError = time.Minute
|
||||||
|
|
||||||
|
type dbScheduler[T entity.DbTask] struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
waitGroup sync.WaitGroup
|
||||||
|
queue *queue.DelayQueue[T]
|
||||||
|
context context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
RunTask func(ctx context.Context, task T) error
|
||||||
|
taskRepo repository.DbTask[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSchedulerOption[T entity.DbTask] func(*dbScheduler[T])
|
||||||
|
|
||||||
|
func newDbScheduler[T entity.DbTask](taskRepo repository.DbTask[T], opts ...dbSchedulerOption[T]) (*dbScheduler[T], error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
scheduler := &dbScheduler[T]{
|
||||||
|
taskRepo: taskRepo,
|
||||||
|
queue: queue.NewDelayQueue[T](0),
|
||||||
|
context: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(scheduler)
|
||||||
|
}
|
||||||
|
if scheduler.RunTask == nil {
|
||||||
|
return nil, errors.New("数据库任务调度器没有设置 RunTask")
|
||||||
|
}
|
||||||
|
if err := scheduler.loadTask(context.Background()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scheduler.waitGroup.Add(1)
|
||||||
|
go scheduler.run()
|
||||||
|
return scheduler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) updateTaskStatus(ctx context.Context, status entity.TaskStatus, lastErr error, task T) error {
|
||||||
|
base := task.GetTaskBase()
|
||||||
|
base.LastStatus = status
|
||||||
|
var result = task.MessageWithStatus(status)
|
||||||
|
if lastErr != nil {
|
||||||
|
result = fmt.Sprintf("%v: %v", result, lastErr)
|
||||||
|
}
|
||||||
|
base.LastResult = stringx.TruncateStr(result, entity.LastResultSize)
|
||||||
|
base.LastTime = timex.NewNullTime(time.Now())
|
||||||
|
return s.taskRepo.UpdateTaskStatus(ctx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) UpdateTask(ctx context.Context, task T) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if err := s.taskRepo.UpdateById(ctx, task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTask, ok := s.queue.Remove(ctx, task.GetId())
|
||||||
|
if !ok {
|
||||||
|
return errors.New("任务不存在")
|
||||||
|
}
|
||||||
|
oldTask.Update(task)
|
||||||
|
if !oldTask.Schedule() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !s.queue.Enqueue(ctx, oldTask) {
|
||||||
|
return errors.New("任务入队失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) run() {
|
||||||
|
defer s.waitGroup.Done()
|
||||||
|
|
||||||
|
for !s.closed() {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
task, ok := s.queue.TryDequeue()
|
||||||
|
if !ok {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.updateTaskStatus(s.context, entity.TaskReserved, nil, task); err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
timex.SleepWithContext(s.context, sleepAfterError)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
errRun := s.RunTask(s.context, task)
|
||||||
|
taskStatus := entity.TaskSuccess
|
||||||
|
if errRun != nil {
|
||||||
|
taskStatus = entity.TaskFailed
|
||||||
|
}
|
||||||
|
s.mutex.Lock()
|
||||||
|
if err := s.updateTaskStatus(s.context, taskStatus, errRun, task); err != nil {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
timex.SleepWithContext(s.context, sleepAfterError)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
task.Schedule()
|
||||||
|
if !task.IsFinished() {
|
||||||
|
s.queue.Enqueue(s.context, task)
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) Close() {
|
||||||
|
s.cancel()
|
||||||
|
s.waitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) closed() bool {
|
||||||
|
return s.context.Err() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) loadTask(ctx context.Context) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
tasks, err := s.taskRepo.ListToDo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if !task.Schedule() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.queue.Enqueue(ctx, task)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) AddTask(ctx context.Context, tasks ...T) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
if err := s.taskRepo.AddTask(ctx, task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !task.Schedule() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.queue.Enqueue(ctx, task)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) DeleteTask(ctx context.Context, taskId uint64) error {
|
||||||
|
// todo: 删除数据库备份历史文件
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if err := s.taskRepo.DeleteById(ctx, taskId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.queue.Remove(ctx, taskId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) EnableTask(ctx context.Context, taskId uint64) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
task := anyx.DeepZero[T]()
|
||||||
|
if err := s.taskRepo.GetById(task, taskId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if task.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
task.GetTaskBase().Enabled = true
|
||||||
|
if err := s.taskRepo.UpdateEnabled(ctx, taskId, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.queue.Remove(ctx, taskId)
|
||||||
|
if !task.Schedule() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.queue.Enqueue(ctx, task)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) DisableTask(ctx context.Context, taskId uint64) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
task := anyx.DeepZero[T]()
|
||||||
|
if err := s.taskRepo.GetById(task, taskId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !task.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.taskRepo.UpdateEnabled(ctx, taskId, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.queue.Remove(ctx, taskId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler[T]) StartTask(ctx context.Context, taskId uint64) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
task := anyx.DeepZero[T]()
|
||||||
|
if err := s.taskRepo.GetById(task, taskId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !task.IsEnabled() {
|
||||||
|
return errors.New("任务未启用")
|
||||||
|
}
|
||||||
|
s.queue.Remove(ctx, taskId)
|
||||||
|
task.GetTaskBase().Deadline = time.Now()
|
||||||
|
s.queue.Enqueue(ctx, task)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"mayfly-go/internal/db/dbm"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
"mayfly-go/pkg/base"
|
||||||
@@ -77,7 +78,9 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return errorx.NewBiz("该数据库实例已存在")
|
return errorx.NewBiz("该数据库实例已存在")
|
||||||
}
|
}
|
||||||
instanceEntity.PwdEncrypt()
|
if err := instanceEntity.PwdEncrypt(); err != nil {
|
||||||
|
return errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
return app.Insert(ctx, instanceEntity)
|
return app.Insert(ctx, instanceEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +88,9 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
|||||||
if err == nil && oldInstance.Id != instanceEntity.Id {
|
if err == nil && oldInstance.Id != instanceEntity.Id {
|
||||||
return errorx.NewBiz("该数据库实例已存在")
|
return errorx.NewBiz("该数据库实例已存在")
|
||||||
}
|
}
|
||||||
instanceEntity.PwdEncrypt()
|
if err := instanceEntity.PwdEncrypt(); err != nil {
|
||||||
|
return errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
return app.UpdateById(ctx, instanceEntity)
|
return app.UpdateById(ctx, instanceEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +100,7 @@ func (app *instanceAppImpl) Delete(ctx context.Context, id uint64) error {
|
|||||||
|
|
||||||
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
||||||
ed.Network = ed.GetNetwork()
|
ed.Network = ed.GetNetwork()
|
||||||
metaDb := ed.Type.MetaDbName()
|
metaDb := dbm.ToDbType(ed.Type).MetaDbName()
|
||||||
|
|
||||||
dbConn, err := toDbInfo(ed, 0, metaDb, "").Conn()
|
dbConn, err := toDbInfo(ed, 0, metaDb, "").Conn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -9,6 +11,7 @@ const (
|
|||||||
ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量
|
ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量
|
||||||
ConfigKeyDbBackupRestore string = "DbBackupRestore" // 数据库备份
|
ConfigKeyDbBackupRestore string = "DbBackupRestore" // 数据库备份
|
||||||
ConfigKeyDbMysqlBin string = "MysqlBin" // mysql可执行文件配置
|
ConfigKeyDbMysqlBin string = "MysqlBin" // mysql可执行文件配置
|
||||||
|
ConfigKeyDbMariaDbBin string = "MariaDbBin" // mariadb可执行文件配置
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取数据库最大查询数量配置
|
// 获取数据库最大查询数量配置
|
||||||
@@ -36,7 +39,7 @@ func GetDbBackupRestore() *DbBackupRestore {
|
|||||||
if backupPath == "" {
|
if backupPath == "" {
|
||||||
backupPath = "./db/backup"
|
backupPath = "./db/backup"
|
||||||
}
|
}
|
||||||
dbrc.BackupPath = backupPath
|
dbrc.BackupPath = filepath.Join(backupPath)
|
||||||
|
|
||||||
return dbrc
|
return dbrc
|
||||||
}
|
}
|
||||||
@@ -50,35 +53,39 @@ type MysqlBin struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库备份配置
|
// 获取数据库备份配置
|
||||||
func GetMysqlBin() *MysqlBin {
|
func GetMysqlBin(configKey string) *MysqlBin {
|
||||||
c := sysapp.GetConfigApp().GetConfig(ConfigKeyDbMysqlBin)
|
c := sysapp.GetConfigApp().GetConfig(configKey)
|
||||||
jm := c.GetJsonMap()
|
jm := c.GetJsonMap()
|
||||||
|
|
||||||
mbc := new(MysqlBin)
|
mbc := new(MysqlBin)
|
||||||
|
|
||||||
path := jm["path"]
|
path := jm["path"]
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "./db/backup"
|
path = "./db/mysql/bin"
|
||||||
}
|
}
|
||||||
mbc.Path = path
|
mbc.Path = filepath.Join(path)
|
||||||
|
|
||||||
|
var extName string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
extName = ".exe"
|
||||||
|
}
|
||||||
mysqlPath := jm["mysql"]
|
mysqlPath := jm["mysql"]
|
||||||
if mysqlPath == "" {
|
if mysqlPath == "" {
|
||||||
mysqlPath = path + "mysql"
|
mysqlPath = filepath.Join(path, "mysql"+extName)
|
||||||
}
|
}
|
||||||
mbc.MysqlPath = mysqlPath
|
mbc.MysqlPath = filepath.Join(mysqlPath)
|
||||||
|
|
||||||
mysqldumpPath := jm["mysqldump"]
|
mysqldumpPath := jm["mysqldump"]
|
||||||
if mysqldumpPath == "" {
|
if mysqldumpPath == "" {
|
||||||
mysqldumpPath = path + "mysqldump"
|
mysqldumpPath = filepath.Join(path, "mysqldump"+extName)
|
||||||
}
|
}
|
||||||
mbc.MysqldumpPath = mysqldumpPath
|
mbc.MysqldumpPath = filepath.Join(mysqldumpPath)
|
||||||
|
|
||||||
mysqlbinlogPath := jm["mysqlbinlog"]
|
mysqlbinlogPath := jm["mysqlbinlog"]
|
||||||
if mysqlbinlogPath == "" {
|
if mysqlbinlogPath == "" {
|
||||||
mysqlbinlogPath = path + "mysqlbinlog"
|
mysqlbinlogPath = filepath.Join(path, "mysqlbinlog"+extName)
|
||||||
}
|
}
|
||||||
mbc.MysqlbinlogPath = mysqlbinlogPath
|
mbc.MysqlbinlogPath = filepath.Join(mysqlbinlogPath)
|
||||||
|
|
||||||
return mbc
|
return mbc
|
||||||
}
|
}
|
||||||
|
|||||||
32
server/internal/db/dbm/db_program.go
Normal file
32
server/internal/db/dbm/db_program.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dbm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/internal/db/domain/entity"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DbProgram interface {
|
||||||
|
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
||||||
|
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error)
|
||||||
|
ReplayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) error
|
||||||
|
RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||||
|
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RestoreInfo struct {
|
||||||
|
BackupHistory *entity.DbBackupHistory
|
||||||
|
BinlogHistories []*entity.DbBinlogHistory
|
||||||
|
StartPosition int64
|
||||||
|
TargetPosition int64
|
||||||
|
TargetTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *RestoreInfo) GetBinlogPaths(binlogDir string) []string {
|
||||||
|
files := make([]string, 0, len(ri.BinlogHistories))
|
||||||
|
for _, history := range ri.BinlogHistories {
|
||||||
|
files = append(files, filepath.Join(binlogDir, history.FileName))
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package dbm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -19,112 +19,55 @@ import (
|
|||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"mayfly-go/internal/db/config"
|
"mayfly-go/internal/db/config"
|
||||||
"mayfly-go/internal/db/dbm"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
|
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/utils/structx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BinlogFile is the metadata of the MySQL binlog file.
|
var _ DbProgram = (*DbProgramMysql)(nil)
|
||||||
type BinlogFile struct {
|
|
||||||
Name string
|
|
||||||
Size int64
|
|
||||||
|
|
||||||
// Sequence is parsed from Name and is for the sorting purpose.
|
type DbProgramMysql struct {
|
||||||
Sequence int64
|
dbConn *DbConn
|
||||||
FirstEventTime time.Time
|
// mysqlBin 用于集成测试
|
||||||
Downloaded bool
|
mysqlBin *config.MysqlBin
|
||||||
|
// backupPath 用于集成测试
|
||||||
|
backupPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBinlogFile(name string, size int64) (*BinlogFile, error) {
|
func NewDbProgramMysql(dbConn *DbConn) *DbProgramMysql {
|
||||||
_, seq, err := ParseBinlogName(name)
|
return &DbProgramMysql{
|
||||||
if err != nil {
|
dbConn: dbConn,
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &BinlogFile{Name: name, Size: size, Sequence: seq}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ service.DbInstanceSvc = (*DbInstanceSvcImpl)(nil)
|
|
||||||
|
|
||||||
type DbInstanceSvcImpl struct {
|
|
||||||
instanceId uint64
|
|
||||||
dbInfo *dbm.DbInfo
|
|
||||||
backupHistoryRepo repository.DbBackupHistory
|
|
||||||
binlogHistoryRepo repository.DbBinlogHistory
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDbInstanceSvc(instance *entity.DbInstance, repositories *repository.Repositories) *DbInstanceSvcImpl {
|
|
||||||
dbInfo := new(dbm.DbInfo)
|
|
||||||
_ = structx.Copy(dbInfo, instance)
|
|
||||||
return &DbInstanceSvcImpl{
|
|
||||||
instanceId: instance.Id,
|
|
||||||
dbInfo: dbInfo,
|
|
||||||
backupHistoryRepo: repositories.BackupHistory,
|
|
||||||
binlogHistoryRepo: repositories.BinlogHistory,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestoreInfo struct {
|
func (svc *DbProgramMysql) dbInfo() *DbInfo {
|
||||||
backupHistory *entity.DbBackupHistory
|
return svc.dbConn.Info
|
||||||
binlogHistories []*entity.DbBinlogHistory
|
|
||||||
startPosition int64
|
|
||||||
targetPosition int64
|
|
||||||
targetTime time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ri *RestoreInfo) getBinlogFiles(binlogDir string) []string {
|
func (svc *DbProgramMysql) getMysqlBin() *config.MysqlBin {
|
||||||
files := make([]string, 0, len(ri.binlogHistories))
|
if svc.mysqlBin != nil {
|
||||||
for _, history := range ri.binlogHistories {
|
return svc.mysqlBin
|
||||||
files = append(files, filepath.Join(binlogDir, history.FileName))
|
|
||||||
}
|
}
|
||||||
return files
|
var mysqlBin *config.MysqlBin
|
||||||
|
switch svc.dbInfo().Type {
|
||||||
|
default:
|
||||||
|
mysqlBin = config.GetMysqlBin(config.ConfigKeyDbMysqlBin)
|
||||||
|
}
|
||||||
|
return mysqlBin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) getBinlogFilePath(fileName string) string {
|
func (svc *DbProgramMysql) getBackupPath() string {
|
||||||
return filepath.Join(getBinlogDir(svc.instanceId), fileName)
|
if len(svc.backupPath) > 0 {
|
||||||
|
return svc.backupPath
|
||||||
|
}
|
||||||
|
return config.GetDbBackupRestore().BackupPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) GetRestoreInfo(ctx context.Context, dbName string, targetTime time.Time) (*RestoreInfo, error) {
|
func (svc *DbProgramMysql) GetBinlogFilePath(fileName string) string {
|
||||||
binlogHistory, err := svc.binlogHistoryRepo.GetHistoryByTime(svc.instanceId, targetTime)
|
return filepath.Join(svc.getBinlogDir(svc.dbInfo().InstanceId), fileName)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
position, err := getBinlogEventPositionAtOrAfterTime(ctx, svc.getBinlogFilePath(binlogHistory.FileName), targetTime)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
target := &entity.BinlogInfo{
|
|
||||||
FileName: binlogHistory.FileName,
|
|
||||||
Sequence: binlogHistory.Sequence,
|
|
||||||
Position: position,
|
|
||||||
}
|
|
||||||
backupHistory, err := svc.backupHistoryRepo.GetLatestHistory(svc.instanceId, dbName, target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
start := &entity.BinlogInfo{
|
|
||||||
FileName: backupHistory.BinlogFileName,
|
|
||||||
Sequence: backupHistory.BinlogSequence,
|
|
||||||
Position: backupHistory.BinlogPosition,
|
|
||||||
}
|
|
||||||
binlogHistories, err := svc.binlogHistoryRepo.GetHistories(svc.instanceId, start, target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &RestoreInfo{
|
|
||||||
backupHistory: backupHistory,
|
|
||||||
binlogHistories: binlogHistories,
|
|
||||||
startPosition: backupHistory.BinlogPosition,
|
|
||||||
targetPosition: target.Position,
|
|
||||||
targetTime: targetTime,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error) {
|
func (svc *DbProgramMysql) Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error) {
|
||||||
dir := getDbBackupDir(backupHistory.DbInstanceId, backupHistory.DbBackupId)
|
dir := svc.getDbBackupDir(backupHistory.DbInstanceId, backupHistory.DbBackupId)
|
||||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -134,10 +77,10 @@ func (svc *DbInstanceSvcImpl) Backup(ctx context.Context, backupHistory *entity.
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"--host", svc.dbInfo.Host,
|
"--host", svc.dbInfo().Host,
|
||||||
"--port", strconv.Itoa(svc.dbInfo.Port),
|
"--port", strconv.Itoa(svc.dbInfo().Port),
|
||||||
"--user", svc.dbInfo.Username,
|
"--user", svc.dbInfo().Username,
|
||||||
"--password=" + svc.dbInfo.Password,
|
"--password=" + svc.dbInfo().Password,
|
||||||
"--add-drop-database",
|
"--add-drop-database",
|
||||||
"--result-file", tmpFile,
|
"--result-file", tmpFile,
|
||||||
"--single-transaction",
|
"--single-transaction",
|
||||||
@@ -145,7 +88,7 @@ func (svc *DbInstanceSvcImpl) Backup(ctx context.Context, backupHistory *entity.
|
|||||||
"--databases", backupHistory.DbName,
|
"--databases", backupHistory.DbName,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, mysqldumpPath(), args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqldumpPath, args...)
|
||||||
logx.Debugf("backup database using mysqldump binary: %s", cmd.String())
|
logx.Debugf("backup database using mysqldump binary: %s", cmd.String())
|
||||||
if err := runCmd(cmd); err != nil {
|
if err := runCmd(cmd); err != nil {
|
||||||
logx.Errorf("运行 mysqldump 程序失败: %v", err)
|
logx.Errorf("运行 mysqldump 程序失败: %v", err)
|
||||||
@@ -174,15 +117,17 @@ func (svc *DbInstanceSvcImpl) Backup(ctx context.Context, backupHistory *entity.
|
|||||||
return binlogInfo, nil
|
return binlogInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) RestoreBackup(ctx context.Context, database, fileName string) error {
|
func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
||||||
args := []string{
|
args := []string{
|
||||||
"--host", svc.dbInfo.Host,
|
"--host", svc.dbInfo().Host,
|
||||||
"--port", strconv.Itoa(svc.dbInfo.Port),
|
"--port", strconv.Itoa(svc.dbInfo().Port),
|
||||||
"--database", database,
|
"--database", dbName,
|
||||||
"--user", svc.dbInfo.Username,
|
"--user", svc.dbInfo().Username,
|
||||||
"--password=" + svc.dbInfo.Password,
|
"--password=" + svc.dbInfo().Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
||||||
|
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
||||||
file, err := os.Open(fileName)
|
file, err := os.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "打开备份文件失败")
|
return errors.Wrap(err, "打开备份文件失败")
|
||||||
@@ -191,7 +136,7 @@ func (svc *DbInstanceSvcImpl) RestoreBackup(ctx context.Context, database, fileN
|
|||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, mysqlPath(), args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlPath, args...)
|
||||||
cmd.Stdin = file
|
cmd.Stdin = file
|
||||||
logx.Debug("恢复数据库: ", cmd.String())
|
logx.Debug("恢复数据库: ", cmd.String())
|
||||||
if err := runCmd(cmd); err != nil {
|
if err := runCmd(cmd); err != nil {
|
||||||
@@ -201,42 +146,14 @@ func (svc *DbInstanceSvcImpl) RestoreBackup(ctx context.Context, database, fileN
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) Restore(ctx context.Context, task *entity.DbRestore) error {
|
|
||||||
if task.PointInTime.IsZero() {
|
|
||||||
backupHistory := &entity.DbBackupHistory{}
|
|
||||||
err := svc.backupHistoryRepo.GetById(backupHistory, task.DbBackupHistoryId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Join(getDbBackupDir(backupHistory.DbInstanceId, backupHistory.DbBackupId),
|
|
||||||
fmt.Sprintf("%v.sql", backupHistory.Uuid))
|
|
||||||
return svc.RestoreBackup(ctx, task.DbName, fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := svc.FetchBinlogs(ctx, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
restoreInfo, err := svc.GetRestoreInfo(ctx, task.DbName, task.PointInTime)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Join(getDbBackupDir(restoreInfo.backupHistory.DbInstanceId, restoreInfo.backupHistory.DbBackupId),
|
|
||||||
fmt.Sprintf("%s.sql", restoreInfo.backupHistory.Uuid))
|
|
||||||
|
|
||||||
if err := svc.RestoreBackup(ctx, task.DbName, fileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return svc.ReplayBinlogToDatabase(ctx, task.DbName, task.DbName, restoreInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download binlog files on server.
|
// Download binlog files on server.
|
||||||
func (svc *DbInstanceSvcImpl) downloadBinlogFilesOnServer(ctx context.Context, binlogFilesOnServerSorted []*BinlogFile, downloadLatestBinlogFile bool) error {
|
func (svc *DbProgramMysql) downloadBinlogFilesOnServer(ctx context.Context, binlogFilesOnServerSorted []*entity.BinlogFile, downloadLatestBinlogFile bool) error {
|
||||||
if len(binlogFilesOnServerSorted) == 0 {
|
if len(binlogFilesOnServerSorted) == 0 {
|
||||||
logx.Debug("No binlog file found on server to download")
|
logx.Debug("No binlog file found on server to download")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(getBinlogDir(svc.instanceId), os.ModePerm); err != nil {
|
if err := os.MkdirAll(svc.getBinlogDir(svc.dbInfo().InstanceId), os.ModePerm); err != nil {
|
||||||
return errors.Wrapf(err, "创建 binlog 目录失败: %q", getBinlogDir(svc.instanceId))
|
return errors.Wrapf(err, "创建 binlog 目录失败: %q", svc.getBinlogDir(svc.dbInfo().InstanceId))
|
||||||
}
|
}
|
||||||
latestBinlogFileOnServer := binlogFilesOnServerSorted[len(binlogFilesOnServerSorted)-1]
|
latestBinlogFileOnServer := binlogFilesOnServerSorted[len(binlogFilesOnServerSorted)-1]
|
||||||
for _, fileOnServer := range binlogFilesOnServerSorted {
|
for _, fileOnServer := range binlogFilesOnServerSorted {
|
||||||
@@ -244,7 +161,7 @@ func (svc *DbInstanceSvcImpl) downloadBinlogFilesOnServer(ctx context.Context, b
|
|||||||
if isLatest && !downloadLatestBinlogFile {
|
if isLatest && !downloadLatestBinlogFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
binlogFilePath := filepath.Join(getBinlogDir(svc.instanceId), fileOnServer.Name)
|
binlogFilePath := filepath.Join(svc.getBinlogDir(svc.dbInfo().InstanceId), fileOnServer.Name)
|
||||||
logx.Debug("Downloading binlog file from MySQL server.", logx.String("path", binlogFilePath), logx.Bool("isLatest", isLatest))
|
logx.Debug("Downloading binlog file from MySQL server.", logx.String("path", binlogFilePath), logx.Bool("isLatest", isLatest))
|
||||||
if err := svc.downloadBinlogFile(ctx, fileOnServer, isLatest); err != nil {
|
if err := svc.downloadBinlogFile(ctx, fileOnServer, isLatest); err != nil {
|
||||||
logx.Error("下载 binlog 文件失败", logx.String("path", binlogFilePath), logx.String("error", err.Error()))
|
logx.Error("下载 binlog 文件失败", logx.String("path", binlogFilePath), logx.String("error", err.Error()))
|
||||||
@@ -255,7 +172,7 @@ func (svc *DbInstanceSvcImpl) downloadBinlogFilesOnServer(ctx context.Context, b
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the first binlog eventTs of a local binlog file.
|
// Parse the first binlog eventTs of a local binlog file.
|
||||||
func parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (eventTime time.Time, parseErr error) {
|
func (svc *DbProgramMysql) parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (eventTime time.Time, parseErr error) {
|
||||||
args := []string{
|
args := []string{
|
||||||
// Local binlog file path.
|
// Local binlog file path.
|
||||||
filePath,
|
filePath,
|
||||||
@@ -264,7 +181,7 @@ func parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (event
|
|||||||
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
||||||
"--base64-output=DECODE-ROWS",
|
"--base64-output=DECODE-ROWS",
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, mysqlbinlogPath(), args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, args...)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
pr, err := cmd.StdoutPipe()
|
pr, err := cmd.StdoutPipe()
|
||||||
@@ -282,7 +199,7 @@ func parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (event
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for s := bufio.NewScanner(pr); ; s.Scan() {
|
for s := bufio.NewScanner(pr); s.Scan(); {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
eventTimeParsed, found, err := parseBinlogEventTimeInLine(line)
|
eventTimeParsed, found, err := parseBinlogEventTimeInLine(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -295,89 +212,58 @@ func parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (event
|
|||||||
return time.Time{}, errors.New("解析 binlog 文件失败")
|
return time.Time{}, errors.New("解析 binlog 文件失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBinlogDir gets the binlogDir.
|
|
||||||
func getBinlogDir(instanceId uint64) string {
|
|
||||||
return filepath.Join(
|
|
||||||
config.GetDbBackupRestore().BackupPath,
|
|
||||||
fmt.Sprintf("instance-%d", instanceId),
|
|
||||||
"binlog")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDbInstanceBackupRoot(instanceId uint64) string {
|
|
||||||
return filepath.Join(
|
|
||||||
config.GetDbBackupRestore().BackupPath,
|
|
||||||
fmt.Sprintf("instance-%d", instanceId))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDbBackupDir(instanceId, backupId uint64) string {
|
|
||||||
return filepath.Join(
|
|
||||||
config.GetDbBackupRestore().BackupPath,
|
|
||||||
fmt.Sprintf("instance-%d", instanceId),
|
|
||||||
fmt.Sprintf("backup-%d", backupId))
|
|
||||||
}
|
|
||||||
|
|
||||||
var singleFlightGroup singleflight.Group
|
var singleFlightGroup singleflight.Group
|
||||||
|
|
||||||
// FetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
// FetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
||||||
func (svc *DbInstanceSvcImpl) FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool) error {
|
func (svc *DbProgramMysql) FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error) {
|
||||||
latestDownloaded := false
|
var downloaded bool
|
||||||
_, err, _ := singleFlightGroup.Do(strconv.FormatUint(svc.instanceId, 10), func() (interface{}, error) {
|
key := strconv.FormatUint(svc.dbInfo().InstanceId, 16)
|
||||||
latestDownloaded = downloadLatestBinlogFile
|
binlogFiles, err, _ := singleFlightGroup.Do(key, func() (interface{}, error) {
|
||||||
err := svc.fetchBinlogs(ctx, downloadLatestBinlogFile)
|
downloaded = true
|
||||||
return nil, err
|
return svc.fetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
if downloadLatestBinlogFile && !latestDownloaded {
|
return nil, err
|
||||||
_, err, _ = singleFlightGroup.Do(strconv.FormatUint(svc.instanceId, 10), func() (interface{}, error) {
|
|
||||||
err := svc.fetchBinlogs(ctx, true)
|
|
||||||
return nil, err
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
if downloaded {
|
||||||
|
return binlogFiles.([]*entity.BinlogFile), nil
|
||||||
|
}
|
||||||
|
if !downloadLatestBinlogFile {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
binlogFiles, err, _ = singleFlightGroup.Do(key, func() (interface{}, error) {
|
||||||
|
return svc.fetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return binlogFiles.([]*entity.BinlogFile), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
// fetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
||||||
func (svc *DbInstanceSvcImpl) fetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool) error {
|
func (svc *DbProgramMysql) fetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error) {
|
||||||
// Read binlog files list on server.
|
// Read binlog files list on server.
|
||||||
binlogFilesOnServerSorted, err := svc.GetSortedBinlogFilesOnServer(ctx)
|
binlogFilesOnServerSorted, err := svc.GetSortedBinlogFilesOnServer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(binlogFilesOnServerSorted) == 0 {
|
if len(binlogFilesOnServerSorted) == 0 {
|
||||||
logx.Debug("No binlog file found on server to download")
|
logx.Debug("No binlog file found on server to download")
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
|
||||||
latest, ok, err := svc.binlogHistoryRepo.GetLatestHistory(svc.instanceId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
binlogFileName := ""
|
|
||||||
latestSequence := int64(-1)
|
|
||||||
earliestSequence := int64(-1)
|
|
||||||
if ok {
|
|
||||||
latestSequence = latest.Sequence
|
|
||||||
binlogFileName = latest.FileName
|
|
||||||
} else {
|
|
||||||
earliest, err := svc.backupHistoryRepo.GetEarliestHistory(svc.instanceId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
earliestSequence = earliest.BinlogSequence
|
|
||||||
binlogFileName = earliest.BinlogFileName
|
|
||||||
}
|
}
|
||||||
indexHistory := -1
|
indexHistory := -1
|
||||||
for i, file := range binlogFilesOnServerSorted {
|
for i, file := range binlogFilesOnServerSorted {
|
||||||
if latestSequence == file.Sequence {
|
if latestBinlogSequence == file.Sequence {
|
||||||
indexHistory = i + 1
|
indexHistory = i + 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if earliestSequence == file.Sequence {
|
if earliestBackupSequence == file.Sequence {
|
||||||
indexHistory = i
|
indexHistory = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if indexHistory < 0 {
|
if indexHistory < 0 {
|
||||||
return errors.New(fmt.Sprintf("在数据库服务器上未找到 binlog 文件 %q", binlogFileName))
|
return nil, errors.New(fmt.Sprintf("在数据库服务器上未找到 binlog 文件: %d, %d", earliestBackupSequence, latestBinlogSequence))
|
||||||
}
|
}
|
||||||
if indexHistory > len(binlogFilesOnServerSorted)-1 {
|
if indexHistory > len(binlogFilesOnServerSorted)-1 {
|
||||||
indexHistory = len(binlogFilesOnServerSorted) - 1
|
indexHistory = len(binlogFilesOnServerSorted) - 1
|
||||||
@@ -385,58 +271,36 @@ func (svc *DbInstanceSvcImpl) fetchBinlogs(ctx context.Context, downloadLatestBi
|
|||||||
binlogFilesOnServerSorted = binlogFilesOnServerSorted[indexHistory:]
|
binlogFilesOnServerSorted = binlogFilesOnServerSorted[indexHistory:]
|
||||||
|
|
||||||
if err := svc.downloadBinlogFilesOnServer(ctx, binlogFilesOnServerSorted, downloadLatestBinlogFile); err != nil {
|
if err := svc.downloadBinlogFilesOnServer(ctx, binlogFilesOnServerSorted, downloadLatestBinlogFile); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
for i, fileOnServer := range binlogFilesOnServerSorted {
|
|
||||||
if !fileOnServer.Downloaded {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
history := &entity.DbBinlogHistory{
|
|
||||||
CreateTime: time.Now(),
|
|
||||||
FileName: fileOnServer.Name,
|
|
||||||
FileSize: fileOnServer.Size,
|
|
||||||
Sequence: fileOnServer.Sequence,
|
|
||||||
FirstEventTime: fileOnServer.FirstEventTime,
|
|
||||||
DbInstanceId: svc.instanceId,
|
|
||||||
}
|
|
||||||
if i == len(binlogFilesOnServerSorted)-1 {
|
|
||||||
if err := svc.binlogHistoryRepo.Upsert(ctx, history); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := svc.binlogHistoryRepo.Insert(ctx, history); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return binlogFilesOnServerSorted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Syncs the binlog specified by `meta` between the instance and local.
|
// Syncs the binlog specified by `meta` between the instance and local.
|
||||||
// If isLast is true, it means that this is the last binlog file containing the targetTs event.
|
// If isLast is true, it means that this is the last binlog file containing the targetTs event.
|
||||||
// It may keep growing as there are ongoing writes to the database. So we just need to check that
|
// It may keep growing as there are ongoing writes to the database. So we just need to check that
|
||||||
// the file size is larger or equal to the binlog file size we queried from the MySQL server earlier.
|
// the file size is larger or equal to the binlog file size we queried from the MySQL server earlier.
|
||||||
func (svc *DbInstanceSvcImpl) downloadBinlogFile(ctx context.Context, binlogFileToDownload *BinlogFile, isLast bool) error {
|
func (svc *DbProgramMysql) downloadBinlogFile(ctx context.Context, binlogFileToDownload *entity.BinlogFile, isLast bool) error {
|
||||||
tempBinlogPrefix := filepath.Join(getBinlogDir(svc.instanceId), "tmp-")
|
tempBinlogPrefix := filepath.Join(svc.getBinlogDir(svc.dbInfo().InstanceId), "tmp-")
|
||||||
args := []string{
|
args := []string{
|
||||||
binlogFileToDownload.Name,
|
binlogFileToDownload.Name,
|
||||||
"--read-from-remote-server",
|
"--read-from-remote-server",
|
||||||
// Verify checksum binlog events.
|
// Verify checksum binlog events.
|
||||||
"--verify-binlog-checksum",
|
"--verify-binlog-checksum",
|
||||||
"--host", svc.dbInfo.Host,
|
"--host", svc.dbInfo().Host,
|
||||||
"--port", strconv.Itoa(svc.dbInfo.Port),
|
"--port", strconv.Itoa(svc.dbInfo().Port),
|
||||||
"--user", svc.dbInfo.Username,
|
"--user", svc.dbInfo().Username,
|
||||||
"--raw",
|
"--raw",
|
||||||
// With --raw this is a prefix for the file names.
|
// With --raw this is a prefix for the file names.
|
||||||
"--result-file", tempBinlogPrefix,
|
"--result-file", tempBinlogPrefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, mysqlbinlogPath(), args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, args...)
|
||||||
// We cannot set password as a flag. Otherwise, there is warning message
|
// We cannot set password as a flag. Otherwise, there is warning message
|
||||||
// "mysqlbinlog: [Warning] Using a password on the command line interface can be insecure."
|
// "mysqlbinlog: [Warning] Using a password on the command line interface can be insecure."
|
||||||
if svc.dbInfo.Password != "" {
|
if svc.dbInfo().Password != "" {
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("MYSQL_PWD=%s", svc.dbInfo.Password))
|
cmd.Env = append(cmd.Env, fmt.Sprintf("MYSQL_PWD=%s", svc.dbInfo().Password))
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Debug("Downloading binlog files using mysqlbinlog:", cmd.String())
|
logx.Debug("Downloading binlog files using mysqlbinlog:", cmd.String())
|
||||||
@@ -464,11 +328,11 @@ func (svc *DbInstanceSvcImpl) downloadBinlogFile(ctx context.Context, binlogFile
|
|||||||
return errors.Errorf("下载的 binlog 文件 %q 与服务上的文件大小不一致 %d != %d", binlogFilePathTemp, binlogFileTempInfo.Size(), binlogFileToDownload.Size)
|
return errors.Errorf("下载的 binlog 文件 %q 与服务上的文件大小不一致 %d != %d", binlogFilePathTemp, binlogFileTempInfo.Size(), binlogFileToDownload.Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
binlogFilePath := svc.getBinlogFilePath(binlogFileToDownload.Name)
|
binlogFilePath := svc.GetBinlogFilePath(binlogFileToDownload.Name)
|
||||||
if err := os.Rename(binlogFilePathTemp, binlogFilePath); err != nil {
|
if err := os.Rename(binlogFilePathTemp, binlogFilePath); err != nil {
|
||||||
return errors.Wrapf(err, "binlog 文件更名失败: %q -> %q", binlogFilePathTemp, binlogFilePath)
|
return errors.Wrapf(err, "binlog 文件更名失败: %q -> %q", binlogFilePathTemp, binlogFilePath)
|
||||||
}
|
}
|
||||||
firstEventTime, err := parseLocalBinlogFirstEventTime(ctx, binlogFilePath)
|
firstEventTime, err := svc.parseLocalBinlogFirstEventTime(ctx, binlogFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -479,14 +343,9 @@ func (svc *DbInstanceSvcImpl) downloadBinlogFile(ctx context.Context, binlogFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSortedBinlogFilesOnServer returns the information of binlog files in ascending order by their numeric extension.
|
// GetSortedBinlogFilesOnServer returns the information of binlog files in ascending order by their numeric extension.
|
||||||
func (svc *DbInstanceSvcImpl) GetSortedBinlogFilesOnServer(_ context.Context) ([]*BinlogFile, error) {
|
func (svc *DbProgramMysql) GetSortedBinlogFilesOnServer(_ context.Context) ([]*entity.BinlogFile, error) {
|
||||||
conn, err := svc.dbInfo.Conn()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
query := "SHOW BINARY LOGS"
|
query := "SHOW BINARY LOGS"
|
||||||
columns, rows, err := conn.Query(query)
|
columns, rows, err := svc.dbConn.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "SQL 语句 %q 执行失败", query)
|
return nil, errors.Wrapf(err, "SQL 语句 %q 执行失败", query)
|
||||||
}
|
}
|
||||||
@@ -504,7 +363,7 @@ func (svc *DbInstanceSvcImpl) GetSortedBinlogFilesOnServer(_ context.Context) ([
|
|||||||
return nil, errors.Errorf("SQL 语句 %q 执行结果解析失败", query)
|
return nil, errors.Errorf("SQL 语句 %q 执行结果解析失败", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
var binlogFiles []*BinlogFile
|
var binlogFiles []*entity.BinlogFile
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
name, nameOk := row["Log_name"].(string)
|
name, nameOk := row["Log_name"].(string)
|
||||||
@@ -512,11 +371,15 @@ func (svc *DbInstanceSvcImpl) GetSortedBinlogFilesOnServer(_ context.Context) ([
|
|||||||
if !nameOk || !sizeOk {
|
if !nameOk || !sizeOk {
|
||||||
return nil, errors.Errorf("SQL 语句 %q 执行结果解析失败", query)
|
return nil, errors.Errorf("SQL 语句 %q 执行结果解析失败", query)
|
||||||
}
|
}
|
||||||
|
_, seq, err := ParseBinlogName(name)
|
||||||
binlogFile, err := newBinlogFile(name, int64(size))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "SQL 语句 %q 执行结果解析失败", query)
|
return nil, errors.Wrapf(err, "SQL 语句 %q 执行结果解析失败", query)
|
||||||
}
|
}
|
||||||
|
binlogFile := &entity.BinlogFile{
|
||||||
|
Name: name,
|
||||||
|
Size: int64(size),
|
||||||
|
Sequence: seq,
|
||||||
|
}
|
||||||
binlogFiles = append(binlogFiles, binlogFile)
|
binlogFiles = append(binlogFiles, binlogFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,18 +429,19 @@ func readBinlogInfoFromBackup(reader io.Reader) (*entity.BinlogInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use command like mysqlbinlog --start-datetime=targetTs binlog.000001 to parse the first binlog event position with timestamp equal or after targetTs.
|
// Use command like mysqlbinlog --start-datetime=targetTs binlog.000001 to parse the first binlog event position with timestamp equal or after targetTs.
|
||||||
func getBinlogEventPositionAtOrAfterTime(ctx context.Context, filePath string, targetTime time.Time) (position int64, parseErr error) {
|
func (svc *DbProgramMysql) GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error) {
|
||||||
|
binlogPath := svc.GetBinlogFilePath(binlogName)
|
||||||
args := []string{
|
args := []string{
|
||||||
// Local binlog file path.
|
// Local binlog file path.
|
||||||
filePath,
|
binlogPath,
|
||||||
// Verify checksum binlog events.
|
// Verify checksum binlog events.
|
||||||
"--verify-binlog-checksum",
|
"--verify-binlog-checksum",
|
||||||
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
||||||
"--base64-output=DECODE-ROWS",
|
"--base64-output=DECODE-ROWS",
|
||||||
// Instruct mysqlbinlog to start output only after encountering the first binlog event with timestamp equal or after targetTime.
|
// Instruct mysqlbinlog to start output only after encountering the first binlog event with timestamp equal or after targetTime.
|
||||||
"--start-datetime", formatDateTime(targetTime),
|
"--start-datetime", targetTime.Local().Format(time.DateTime),
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, mysqlbinlogPath(), args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, args...)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
pr, err := cmd.StdoutPipe()
|
pr, err := cmd.StdoutPipe()
|
||||||
@@ -594,7 +458,7 @@ func getBinlogEventPositionAtOrAfterTime(ctx context.Context, filePath string, t
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for s := bufio.NewScanner(pr); ; s.Scan() {
|
for s := bufio.NewScanner(pr); s.Scan(); {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
posParsed, found, err := parseBinlogEventPosInLine(line)
|
posParsed, found, err := parseBinlogEventPosInLine(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -605,11 +469,11 @@ func getBinlogEventPositionAtOrAfterTime(ctx context.Context, filePath string, t
|
|||||||
return posParsed, nil
|
return posParsed, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0, errors.Errorf("在 %v 之后没有 binlog 事件", targetTime)
|
return 0, errors.Errorf("在 %s 之后没有 binlog 事件", targetTime.Format(time.DateTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
// replayBinlog replays the binlog for `originDatabase` from `startBinlogInfo.Position` to `targetTs`, read binlog from `binlogDir`.
|
// ReplayBinlog replays the binlog for `originDatabase` from `startBinlogInfo.Position` to `targetTs`, read binlog from `binlogDir`.
|
||||||
func (svc *DbInstanceSvcImpl) replayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) (replayErr error) {
|
func (svc *DbProgramMysql) ReplayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) (replayErr error) {
|
||||||
const (
|
const (
|
||||||
// Variable lower_case_table_names related.
|
// Variable lower_case_table_names related.
|
||||||
|
|
||||||
@@ -657,27 +521,27 @@ func (svc *DbInstanceSvcImpl) replayBinlog(ctx context.Context, originalDatabase
|
|||||||
// List entries for just this database. It's applied after the --rewrite-db option, so we should provide the rewritten database, i.e., pitrDatabase.
|
// List entries for just this database. It's applied after the --rewrite-db option, so we should provide the rewritten database, i.e., pitrDatabase.
|
||||||
"--database", targetDatabase,
|
"--database", targetDatabase,
|
||||||
// Decode binary log from first event with position equal to or greater than argument.
|
// Decode binary log from first event with position equal to or greater than argument.
|
||||||
"--start-position", fmt.Sprintf("%d", restoreInfo.startPosition),
|
"--start-position", fmt.Sprintf("%d", restoreInfo.StartPosition),
|
||||||
// Stop decoding binary log at first event with position equal to or greater than argument.
|
// Stop decoding binary log at first event with position equal to or greater than argument.
|
||||||
"--stop-position", fmt.Sprintf("%d", restoreInfo.targetPosition),
|
"--stop-position", fmt.Sprintf("%d", restoreInfo.TargetPosition),
|
||||||
}
|
}
|
||||||
|
|
||||||
mysqlbinlogArgs = append(mysqlbinlogArgs, restoreInfo.getBinlogFiles(getBinlogDir(svc.instanceId))...)
|
mysqlbinlogArgs = append(mysqlbinlogArgs, restoreInfo.GetBinlogPaths(svc.getBinlogDir(svc.dbInfo().InstanceId))...)
|
||||||
|
|
||||||
mysqlArgs := []string{
|
mysqlArgs := []string{
|
||||||
"--host", svc.dbInfo.Host,
|
"--host", svc.dbInfo().Host,
|
||||||
"--port", strconv.Itoa(svc.dbInfo.Port),
|
"--port", strconv.Itoa(svc.dbInfo().Port),
|
||||||
"--user", svc.dbInfo.Username,
|
"--user", svc.dbInfo().Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.dbInfo.Password != "" {
|
if svc.dbInfo().Password != "" {
|
||||||
// The --password parameter of mysql/mysqlbinlog does not support the "--password PASSWORD" format (split by space).
|
// The --password parameter of mysql/mysqlbinlog does not support the "--password PASSWORD" format (split by space).
|
||||||
// If provided like that, the program will hang.
|
// If provided like that, the program will hang.
|
||||||
mysqlArgs = append(mysqlArgs, fmt.Sprintf("--password=%s", svc.dbInfo.Password))
|
mysqlArgs = append(mysqlArgs, fmt.Sprintf("--password=%s", svc.dbInfo().Password))
|
||||||
}
|
}
|
||||||
|
|
||||||
mysqlbinlogCmd := exec.CommandContext(ctx, mysqlbinlogPath(), mysqlbinlogArgs...)
|
mysqlbinlogCmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, mysqlbinlogArgs...)
|
||||||
mysqlCmd := exec.CommandContext(ctx, mysqlPath(), mysqlArgs...)
|
mysqlCmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlPath, mysqlArgs...)
|
||||||
logx.Debug("Start replay binlog commands.",
|
logx.Debug("Start replay binlog commands.",
|
||||||
logx.String("mysqlbinlog", mysqlbinlogCmd.String()),
|
logx.String("mysqlbinlog", mysqlbinlogCmd.String()),
|
||||||
logx.String("mysql", mysqlCmd.String()))
|
logx.String("mysql", mysqlCmd.String()))
|
||||||
@@ -717,26 +581,15 @@ func (svc *DbInstanceSvcImpl) replayBinlog(ctx context.Context, originalDatabase
|
|||||||
return errors.Wrap(err, "启动 mysql 程序失败")
|
return errors.Wrap(err, "启动 mysql 程序失败")
|
||||||
}
|
}
|
||||||
if err := mysqlCmd.Wait(); err != nil {
|
if err := mysqlCmd.Wait(); err != nil {
|
||||||
return errors.Errorf("运行 mysql 程序失败: %s", mysqlbinlogErr.String())
|
return errors.Errorf("运行 mysql 程序失败: %s", mysqlErr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayBinlogToDatabase replays the binlog of originDatabaseName to the targetDatabaseName.
|
func (svc *DbProgramMysql) getServerVariable(_ context.Context, varName string) (string, error) {
|
||||||
func (svc *DbInstanceSvcImpl) ReplayBinlogToDatabase(ctx context.Context, originDatabaseName, targetDatabaseName string, restoreInfo *RestoreInfo) error {
|
|
||||||
return svc.replayBinlog(ctx, originDatabaseName, targetDatabaseName, restoreInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) getServerVariable(_ context.Context, varName string) (string, error) {
|
|
||||||
conn, err := svc.dbInfo.Conn()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
query := fmt.Sprintf("SHOW VARIABLES LIKE '%s'", varName)
|
query := fmt.Sprintf("SHOW VARIABLES LIKE '%s'", varName)
|
||||||
_, rows, err := conn.Query(query)
|
_, rows, err := svc.dbConn.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -754,7 +607,7 @@ func (svc *DbInstanceSvcImpl) getServerVariable(_ context.Context, varName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckBinlogEnabled checks whether binlog is enabled for the current instance.
|
// CheckBinlogEnabled checks whether binlog is enabled for the current instance.
|
||||||
func (svc *DbInstanceSvcImpl) CheckBinlogEnabled(ctx context.Context) error {
|
func (svc *DbProgramMysql) CheckBinlogEnabled(ctx context.Context) error {
|
||||||
value, err := svc.getServerVariable(ctx, "log_bin")
|
value, err := svc.getServerVariable(ctx, "log_bin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -766,7 +619,7 @@ func (svc *DbInstanceSvcImpl) CheckBinlogEnabled(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckBinlogRowFormat checks whether the binlog format is ROW.
|
// CheckBinlogRowFormat checks whether the binlog format is ROW.
|
||||||
func (svc *DbInstanceSvcImpl) CheckBinlogRowFormat(ctx context.Context) error {
|
func (svc *DbProgramMysql) CheckBinlogRowFormat(ctx context.Context) error {
|
||||||
value, err := svc.getServerVariable(ctx, "binlog_format")
|
value, err := svc.getServerVariable(ctx, "binlog_format")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -790,19 +643,19 @@ func runCmd(cmd *exec.Cmd) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbInstanceSvcImpl) execute(database string, sql string) error {
|
func (svc *DbProgramMysql) execute(database string, sql string) error {
|
||||||
args := []string{
|
args := []string{
|
||||||
"--host", svc.dbInfo.Host,
|
"--host", svc.dbInfo().Host,
|
||||||
"--port", strconv.Itoa(svc.dbInfo.Port),
|
"--port", strconv.Itoa(svc.dbInfo().Port),
|
||||||
"--user", svc.dbInfo.Username,
|
"--user", svc.dbInfo().Username,
|
||||||
"--password=" + svc.dbInfo.Password,
|
"--password=" + svc.dbInfo().Password,
|
||||||
"--execute", sql,
|
"--execute", sql,
|
||||||
}
|
}
|
||||||
if len(database) > 0 {
|
if len(database) > 0 {
|
||||||
args = append(args, database)
|
args = append(args, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(mysqlPath(), args...)
|
cmd := exec.Command(svc.getMysqlBin().MysqlPath, args...)
|
||||||
logx.Debug("execute sql using mysql binary: ", cmd.String())
|
logx.Debug("execute sql using mysql binary: ", cmd.String())
|
||||||
if err := runCmd(cmd); err != nil {
|
if err := runCmd(cmd); err != nil {
|
||||||
logx.Errorf("运行 mysql 程序失败: %v", err)
|
logx.Errorf("运行 mysql 程序失败: %v", err)
|
||||||
@@ -814,8 +667,8 @@ func (svc *DbInstanceSvcImpl) execute(database string, sql string) error {
|
|||||||
// sortBinlogFiles will sort binlog files in ascending order by their numeric extension.
|
// sortBinlogFiles will sort binlog files in ascending order by their numeric extension.
|
||||||
// For mysql binlog, after the serial number reaches 999999, the next serial number will not return to 000000, but 1000000,
|
// For mysql binlog, after the serial number reaches 999999, the next serial number will not return to 000000, but 1000000,
|
||||||
// so we cannot directly use string to compare lexicographical order.
|
// so we cannot directly use string to compare lexicographical order.
|
||||||
func sortBinlogFiles(binlogFiles []*BinlogFile) []*BinlogFile {
|
func sortBinlogFiles(binlogFiles []*entity.BinlogFile) []*entity.BinlogFile {
|
||||||
var sorted []*BinlogFile
|
var sorted []*entity.BinlogFile
|
||||||
sorted = append(sorted, binlogFiles...)
|
sorted = append(sorted, binlogFiles...)
|
||||||
sort.Slice(sorted, func(i, j int) bool {
|
sort.Slice(sorted, func(i, j int) bool {
|
||||||
return sorted[i].Sequence < sorted[j].Sequence
|
return sorted[i].Sequence < sorted[j].Sequence
|
||||||
@@ -879,20 +732,23 @@ func ParseBinlogName(name string) (string, int64, error) {
|
|||||||
return s[0], seq, nil
|
return s[0], seq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatDateTime formats the timestamp to the local time string.
|
// getBinlogDir gets the binlogDir.
|
||||||
func formatDateTime(t time.Time) string {
|
func (svc *DbProgramMysql) getBinlogDir(instanceId uint64) string {
|
||||||
t = t.Local()
|
return filepath.Join(
|
||||||
return fmt.Sprintf("%d-%d-%d %d:%d:%d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
|
svc.getBackupPath(),
|
||||||
|
fmt.Sprintf("instance-%d", instanceId),
|
||||||
|
"binlog")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mysqlPath() string {
|
func (svc *DbProgramMysql) getDbInstanceBackupRoot(instanceId uint64) string {
|
||||||
return config.GetMysqlBin().MysqlPath
|
return filepath.Join(
|
||||||
|
svc.getBackupPath(),
|
||||||
|
fmt.Sprintf("instance-%d", instanceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
func mysqldumpPath() string {
|
func (svc *DbProgramMysql) getDbBackupDir(instanceId, backupId uint64) string {
|
||||||
return config.GetMysqlBin().MysqldumpPath
|
return filepath.Join(
|
||||||
}
|
svc.getBackupPath(),
|
||||||
|
fmt.Sprintf("instance-%d", instanceId),
|
||||||
func mysqlbinlogPath() string {
|
fmt.Sprintf("backup-%d", backupId))
|
||||||
return config.GetMysqlBin().MysqlbinlogPath
|
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
//go:build e2e
|
//go:build e2e
|
||||||
|
|
||||||
package service
|
package dbm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"mayfly-go/internal/db/dbm"
|
"mayfly-go/internal/db/config"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/internal/db/infrastructure/persistence"
|
"mayfly-go/internal/db/infrastructure/persistence"
|
||||||
"mayfly-go/pkg/config"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -30,23 +30,25 @@ const (
|
|||||||
|
|
||||||
type DbInstanceSuite struct {
|
type DbInstanceSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
instance *entity.DbInstance
|
|
||||||
repositories *repository.Repositories
|
repositories *repository.Repositories
|
||||||
instanceSvc *DbInstanceSvcImpl
|
instanceSvc *DbProgramMysql
|
||||||
|
dbConn *DbConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DbInstanceSuite) SetupSuite() {
|
func (s *DbInstanceSuite) SetupSuite() {
|
||||||
if err := chdir("mayfly-go", "server"); err != nil {
|
if err := chdir("mayfly-go", "server"); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
config.Init()
|
dbInfo := DbInfo{
|
||||||
s.instance = &entity.DbInstance{
|
Type: DbTypeMysql,
|
||||||
Type: dbm.DbTypeMysql,
|
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: 3306,
|
Port: 3306,
|
||||||
Username: "test",
|
Username: "test",
|
||||||
Password: "123456",
|
Password: "test",
|
||||||
}
|
}
|
||||||
|
dbConn, err := dbInfo.Conn()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.dbConn = dbConn
|
||||||
s.repositories = &repository.Repositories{
|
s.repositories = &repository.Repositories{
|
||||||
Instance: persistence.GetInstanceRepo(),
|
Instance: persistence.GetInstanceRepo(),
|
||||||
Backup: persistence.NewDbBackupRepo(),
|
Backup: persistence.NewDbBackupRepo(),
|
||||||
@@ -56,7 +58,26 @@ func (s *DbInstanceSuite) SetupSuite() {
|
|||||||
Binlog: persistence.NewDbBinlogRepo(),
|
Binlog: persistence.NewDbBinlogRepo(),
|
||||||
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
|
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
|
||||||
}
|
}
|
||||||
s.instanceSvc = NewDbInstanceSvc(s.instance, s.repositories)
|
s.instanceSvc = NewDbProgramMysql(s.dbConn)
|
||||||
|
var extName string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
extName = ".exe"
|
||||||
|
}
|
||||||
|
path := "db/mysql/bin"
|
||||||
|
s.instanceSvc.mysqlBin = &config.MysqlBin{
|
||||||
|
Path: filepath.Join(path),
|
||||||
|
MysqlPath: filepath.Join(path, "mysql"+extName),
|
||||||
|
MysqldumpPath: filepath.Join(path, "mysqldump"+extName),
|
||||||
|
MysqlbinlogPath: filepath.Join(path, "mysqlbinlog"+extName),
|
||||||
|
}
|
||||||
|
s.instanceSvc.backupPath = "db/backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DbInstanceSuite) TearDownSuite() {
|
||||||
|
if s.dbConn != nil {
|
||||||
|
s.dbConn.Close()
|
||||||
|
s.dbConn = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DbInstanceSuite) SetupTest() {
|
func (s *DbInstanceSuite) SetupTest() {
|
||||||
@@ -72,7 +93,7 @@ func (s *DbInstanceSuite) TearDownTest() {
|
|||||||
sql := fmt.Sprintf("drop database if exists `%s`", dbNameBackupTest)
|
sql := fmt.Sprintf("drop database if exists `%s`", dbNameBackupTest)
|
||||||
require.NoError(s.instanceSvc.execute("", sql))
|
require.NoError(s.instanceSvc.execute("", sql))
|
||||||
|
|
||||||
_ = os.RemoveAll(getDbInstanceBackupRoot(instanceIdTest))
|
_ = os.RemoveAll(s.instanceSvc.getDbInstanceBackupRoot(instanceIdTest))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DbInstanceSuite) TestBackup() {
|
func (s *DbInstanceSuite) TestBackup() {
|
||||||
@@ -89,7 +110,7 @@ func (s *DbInstanceSuite) testBackup(backupHistory *entity.DbBackupHistory) {
|
|||||||
binlogInfo, err := s.instanceSvc.Backup(context.Background(), backupHistory)
|
binlogInfo, err := s.instanceSvc.Backup(context.Background(), backupHistory)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
fileName := filepath.Join(getDbBackupDir(s.instance.Id, backupHistory.Id), dbNameBackupTest+".sql")
|
fileName := filepath.Join(s.instanceSvc.getDbBackupDir(s.dbConn.Info.InstanceId, backupHistory.Id), dbNameBackupTest+".sql")
|
||||||
_, err = os.Stat(fileName)
|
_, err = os.Stat(fileName)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
@@ -165,7 +186,7 @@ func (s *DbInstanceSuite) testReplayBinlog(backupHistory *entity.DbBackupHistory
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
binlogFileLast := binlogFilesOnServerSorted[len(binlogFilesOnServerSorted)-1]
|
binlogFileLast := binlogFilesOnServerSorted[len(binlogFilesOnServerSorted)-1]
|
||||||
position, err := getBinlogEventPositionAtOrAfterTime(context.Background(), s.instanceSvc.getBinlogFilePath(binlogFileLast.Name), targetTime)
|
position, err := s.instanceSvc.GetBinlogEventPositionAtOrAfterTime(context.Background(), binlogFileLast.Name, targetTime)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
binlogHistories := make([]*entity.DbBinlogHistory, 0, 2)
|
binlogHistories := make([]*entity.DbBinlogHistory, 0, 2)
|
||||||
binlogHistoryBackup := &entity.DbBinlogHistory{
|
binlogHistoryBackup := &entity.DbBinlogHistory{
|
||||||
@@ -183,21 +204,19 @@ func (s *DbInstanceSuite) testReplayBinlog(backupHistory *entity.DbBackupHistory
|
|||||||
}
|
}
|
||||||
|
|
||||||
restoreInfo := &RestoreInfo{
|
restoreInfo := &RestoreInfo{
|
||||||
backupHistory: backupHistory,
|
BackupHistory: backupHistory,
|
||||||
binlogHistories: binlogHistories,
|
BinlogHistories: binlogHistories,
|
||||||
startPosition: backupHistory.BinlogPosition,
|
StartPosition: backupHistory.BinlogPosition,
|
||||||
targetPosition: position,
|
TargetPosition: position,
|
||||||
targetTime: targetTime,
|
TargetTime: targetTime,
|
||||||
}
|
}
|
||||||
err = s.instanceSvc.ReplayBinlogToDatabase(context.Background(), dbNameBackupTest, dbNameBackupTest, restoreInfo)
|
err = s.instanceSvc.ReplayBinlog(context.Background(), dbNameBackupTest, dbNameBackupTest, restoreInfo)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DbInstanceSuite) testRestore(backupHistory *entity.DbBackupHistory) {
|
func (s *DbInstanceSuite) testRestore(backupHistory *entity.DbBackupHistory) {
|
||||||
require := s.Require()
|
require := s.Require()
|
||||||
fileName := filepath.Join(getDbBackupDir(instanceIdTest, backupIdTest),
|
err := s.instanceSvc.RestoreBackupHistory(context.Background(), backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||||
fmt.Sprintf("%v.sql", dbNameBackupTest))
|
|
||||||
err := s.instanceSvc.RestoreBackup(context.Background(), dbNameBackupTest, fileName)
|
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package dbm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -16,6 +16,14 @@ const (
|
|||||||
DM DbType = "dm"
|
DM DbType = "dm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ToDbType(dbType string) DbType {
|
||||||
|
return DbType(dbType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbType DbType) Equal(typ string) bool {
|
||||||
|
return ToDbType(typ) == dbType
|
||||||
|
}
|
||||||
|
|
||||||
func (dbType DbType) MetaDbName() string {
|
func (dbType DbType) MetaDbName() string {
|
||||||
switch dbType {
|
switch dbType {
|
||||||
case DbTypeMysql:
|
case DbTypeMysql:
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ type DbDialect interface {
|
|||||||
WalkTableRecord(tableName string, walk func(record map[string]any, columns []*QueryColumn)) error
|
WalkTableRecord(tableName string, walk func(record map[string]any, columns []*QueryColumn)) error
|
||||||
|
|
||||||
GetSchemas() ([]string, error)
|
GetSchemas() ([]string, error)
|
||||||
|
|
||||||
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
|
GetDbProgram() DbProgram
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------- 元数据sql操作 -------------------------
|
// ------------------------- 元数据sql操作 -------------------------
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ func (dd *DMDialect) GetDbServer() (*DbServer, error) {
|
|||||||
return ds, nil
|
return ds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *DMDialect) GetDbNames() ([]string, error) {
|
func (dd *DMDialect) GetDbNames() ([]string, error) {
|
||||||
_, res, err := pd.dc.Query("SELECT name AS DBNAME FROM v$database")
|
_, res, err := dd.dc.Query("SELECT name AS DBNAME FROM v$database")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -83,13 +83,13 @@ func (pd *DMDialect) GetDbNames() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取表基础元信息, 如表名等
|
// 获取表基础元信息, 如表名等
|
||||||
func (pd *DMDialect) GetTables() ([]Table, error) {
|
func (dd *DMDialect) GetTables() ([]Table, error) {
|
||||||
|
|
||||||
// 首先执行更新统计信息sql 这个统计信息在数据量比较大的时候就比较耗时,所以最好定时执行
|
// 首先执行更新统计信息sql 这个统计信息在数据量比较大的时候就比较耗时,所以最好定时执行
|
||||||
// _, _, err := pd.dc.Query("dbms_stats.GATHER_SCHEMA_stats(SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))")
|
// _, _, err := pd.dc.Query("dbms_stats.GATHER_SCHEMA_stats(SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))")
|
||||||
|
|
||||||
// 查询表信息
|
// 查询表信息
|
||||||
_, res, err := pd.dc.Query(GetLocalSql(DM_META_FILE, DM_TABLE_INFO_KEY))
|
_, res, err := dd.dc.Query(GetLocalSql(DM_META_FILE, DM_TABLE_INFO_KEY))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func (pd *DMDialect) GetTables() ([]Table, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取列元信息, 如列名等
|
// 获取列元信息, 如列名等
|
||||||
func (pd *DMDialect) GetColumns(tableNames ...string) ([]Column, error) {
|
func (dd *DMDialect) GetColumns(tableNames ...string) ([]Column, error) {
|
||||||
tableName := ""
|
tableName := ""
|
||||||
for i := 0; i < len(tableNames); i++ {
|
for i := 0; i < len(tableNames); i++ {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
@@ -118,7 +118,7 @@ func (pd *DMDialect) GetColumns(tableNames ...string) ([]Column, error) {
|
|||||||
tableName = tableName + "'" + tableNames[i] + "'"
|
tableName = tableName + "'" + tableNames[i] + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
_, res, err := pd.dc.Query(fmt.Sprintf(GetLocalSql(DM_META_FILE, DM_COLUMN_MA_KEY), tableName))
|
_, res, err := dd.dc.Query(fmt.Sprintf(GetLocalSql(DM_META_FILE, DM_COLUMN_MA_KEY), tableName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -139,8 +139,8 @@ func (pd *DMDialect) GetColumns(tableNames ...string) ([]Column, error) {
|
|||||||
return columns, nil
|
return columns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
|
func (dd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
|
||||||
columns, err := pd.GetColumns(tablename)
|
columns, err := dd.GetColumns(tablename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -157,8 +157,8 @@ func (pd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取表索引信息
|
// 获取表索引信息
|
||||||
func (pd *DMDialect) GetTableIndex(tableName string) ([]Index, error) {
|
func (dd *DMDialect) GetTableIndex(tableName string) ([]Index, error) {
|
||||||
_, res, err := pd.dc.Query(fmt.Sprintf(GetLocalSql(DM_META_FILE, DM_INDEX_INFO_KEY), tableName))
|
_, res, err := dd.dc.Query(fmt.Sprintf(GetLocalSql(DM_META_FILE, DM_INDEX_INFO_KEY), tableName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -194,9 +194,9 @@ func (pd *DMDialect) GetTableIndex(tableName string) ([]Index, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取建表ddl
|
// 获取建表ddl
|
||||||
func (pd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
func (dd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
||||||
ddlSql := fmt.Sprintf("CALL SP_TABLEDEF((SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)), '%s')", tableName)
|
ddlSql := fmt.Sprintf("CALL SP_TABLEDEF((SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)), '%s')", tableName)
|
||||||
_, res, err := pd.dc.Query(ddlSql)
|
_, res, err := dd.dc.Query(ddlSql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ func (pd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表注释
|
// 表注释
|
||||||
_, res, err = pd.dc.Query(fmt.Sprintf(`
|
_, res, err = dd.dc.Query(fmt.Sprintf(`
|
||||||
select OWNER, COMMENTS from DBA_TAB_COMMENTS where TABLE_TYPE='TABLE' and TABLE_NAME = '%s'
|
select OWNER, COMMENTS from DBA_TAB_COMMENTS where TABLE_TYPE='TABLE' and TABLE_NAME = '%s'
|
||||||
and owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
and owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||||
`, tableName))
|
`, tableName))
|
||||||
@@ -229,7 +229,7 @@ func (pd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
|||||||
WHERE OWNER = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
WHERE OWNER = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||||
AND TABLE_NAME = '%s'
|
AND TABLE_NAME = '%s'
|
||||||
`, tableName)
|
`, tableName)
|
||||||
_, res, err = pd.dc.Query(fieldSql)
|
_, res, err = dd.dc.Query(fieldSql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ func (pd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
|||||||
and a.table_name = '%s'
|
and a.table_name = '%s'
|
||||||
and indexdef(b.object_id,1) != '禁止查看系统定义的索引信息'
|
and indexdef(b.object_id,1) != '禁止查看系统定义的索引信息'
|
||||||
`, tableName)
|
`, tableName)
|
||||||
_, res, err = pd.dc.Query(indexSql)
|
_, res, err = dd.dc.Query(indexSql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -262,18 +262,18 @@ func (pd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
|||||||
return builder.String(), nil
|
return builder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *DMDialect) GetTableRecord(tableName string, pageNum, pageSize int) ([]*QueryColumn, []map[string]any, error) {
|
func (dd *DMDialect) GetTableRecord(tableName string, pageNum, pageSize int) ([]*QueryColumn, []map[string]any, error) {
|
||||||
return pd.dc.Query(fmt.Sprintf("SELECT * FROM %s OFFSET %d LIMIT %d", tableName, (pageNum-1)*pageSize, pageSize))
|
return dd.dc.Query(fmt.Sprintf("SELECT * FROM %s OFFSET %d LIMIT %d", tableName, (pageNum-1)*pageSize, pageSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *DMDialect) WalkTableRecord(tableName string, walk func(record map[string]any, columns []*QueryColumn)) error {
|
func (dd *DMDialect) WalkTableRecord(tableName string, walk func(record map[string]any, columns []*QueryColumn)) error {
|
||||||
return pd.dc.WalkTableRecord(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walk)
|
return dd.dc.WalkTableRecord(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walk)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取DM当前连接的库可访问的schemaNames
|
// 获取DM当前连接的库可访问的schemaNames
|
||||||
func (pd *DMDialect) GetSchemas() ([]string, error) {
|
func (dd *DMDialect) GetSchemas() ([]string, error) {
|
||||||
sql := GetLocalSql(DM_META_FILE, DM_DB_SCHEMAS)
|
sql := GetLocalSql(DM_META_FILE, DM_DB_SCHEMAS)
|
||||||
_, res, err := pd.dc.Query(sql)
|
_, res, err := dd.dc.Query(sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -283,3 +283,8 @@ func (pd *DMDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
return schemaNames, nil
|
return schemaNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
|
func (dd *DMDialect) GetDbProgram() DbProgram {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,6 +194,11 @@ func (md *MysqlDialect) WalkTableRecord(tableName string, walk func(record map[s
|
|||||||
return md.dc.WalkTableRecord(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walk)
|
return md.dc.WalkTableRecord(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walk)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pd *MysqlDialect) GetSchemas() ([]string, error) {
|
func (md *MysqlDialect) GetSchemas() ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
|
func (md *MysqlDialect) GetDbProgram() DbProgram {
|
||||||
|
return NewDbProgramMysql(md.dc)
|
||||||
|
}
|
||||||
|
|||||||
@@ -277,3 +277,8 @@ func (pd *PgsqlDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
return schemaNames, nil
|
return schemaNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
|
func (pd *PgsqlDialect) GetDbProgram() DbProgram {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,77 +1,39 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ DbTask = (*DbBackup)(nil)
|
var _ DbTask = (*DbBackup)(nil)
|
||||||
|
|
||||||
// DbBackup 数据库备份任务
|
// DbBackup 数据库备份任务
|
||||||
type DbBackup struct {
|
type DbBackup struct {
|
||||||
model.Model
|
*DbTaskBase
|
||||||
|
|
||||||
Name string `gorm:"column(db_name)" json:"name"` // 备份任务名称
|
Name string `json:"name"` // 备份任务名称
|
||||||
DbName string `gorm:"column(db_name)" json:"dbName"` // 数据库名
|
DbName string `json:"dbName"` // 数据库名
|
||||||
StartTime time.Time `gorm:"column(start_time)" json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
Interval time.Duration `gorm:"column(interval)" json:"interval"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
|
||||||
Enabled bool `gorm:"column(enabled)" json:"enabled"` // 是否启用
|
|
||||||
Finished bool `gorm:"column(finished)" json:"finished"` // 是否完成
|
|
||||||
Repeated bool `gorm:"column(repeated)" json:"repeated"` // 是否重复执行
|
|
||||||
LastStatus TaskStatus `gorm:"column(last_status)" json:"lastStatus"` // 最近一次执行状态
|
|
||||||
LastResult string `gorm:"column(last_result)" json:"lastResult"` // 最近一次执行结果
|
|
||||||
LastTime time.Time `gorm:"column(last_time)" json:"lastTime"` // 最近一次执行时间: 2023-11-08 02:00:00
|
|
||||||
DbInstanceId uint64 `gorm:"column(db_instance_id)" json:"dbInstanceId"`
|
|
||||||
Deadline time.Time `gorm:"-" json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbBackup) TableName() string {
|
var (
|
||||||
return "t_db_backup"
|
backupResult = map[TaskStatus]string{
|
||||||
}
|
TaskDelay: "等待备份数据库",
|
||||||
|
TaskReady: "准备备份数据库",
|
||||||
func (d *DbBackup) GetId() uint64 {
|
TaskReserved: "数据库备份中",
|
||||||
if d == nil {
|
TaskSuccess: "数据库备份成功",
|
||||||
return 0
|
TaskFailed: "数据库备份失败",
|
||||||
}
|
}
|
||||||
return d.Id
|
)
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBackup) GetDeadline() time.Time {
|
func (*DbBackup) MessageWithStatus(status TaskStatus) string {
|
||||||
return d.Deadline
|
var result string
|
||||||
}
|
switch status {
|
||||||
|
case TaskDelay:
|
||||||
func (d *DbBackup) Schedule() bool {
|
result = "等待备份数据库"
|
||||||
if d.Finished || !d.Enabled {
|
case TaskReady:
|
||||||
return false
|
result = "准备备份数据库"
|
||||||
}
|
case TaskReserved:
|
||||||
switch d.LastStatus {
|
result = "数据库备份中"
|
||||||
case TaskSuccess:
|
case TaskSuccess:
|
||||||
if d.Interval == 0 {
|
result = "数据库备份成功"
|
||||||
return false
|
|
||||||
}
|
|
||||||
lastTime := d.LastTime
|
|
||||||
if d.LastTime.Sub(d.StartTime) < 0 {
|
|
||||||
lastTime = d.StartTime.Add(-d.Interval)
|
|
||||||
}
|
|
||||||
d.Deadline = lastTime.Add(d.Interval - d.LastTime.Sub(d.StartTime)%d.Interval)
|
|
||||||
case TaskFailed:
|
case TaskFailed:
|
||||||
d.Deadline = time.Now().Add(time.Minute)
|
result = "数据库备份失败"
|
||||||
default:
|
|
||||||
d.Deadline = d.StartTime
|
|
||||||
}
|
}
|
||||||
return true
|
return result
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBackup) IsFinished() bool {
|
|
||||||
return !d.Repeated && d.LastStatus == TaskSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBackup) Update(task DbTask) bool {
|
|
||||||
switch t := task.(type) {
|
|
||||||
case *DbBackup:
|
|
||||||
d.StartTime = t.StartTime
|
|
||||||
d.Interval = t.Interval
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,86 +2,34 @@ package entity
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const BinlogDownloadInterval = time.Minute * 15
|
|
||||||
|
|
||||||
var _ DbTask = (*DbBinlog)(nil)
|
|
||||||
|
|
||||||
// DbBinlog 数据库备份任务
|
// DbBinlog 数据库备份任务
|
||||||
type DbBinlog struct {
|
type DbBinlog struct {
|
||||||
model.Model
|
model.Model
|
||||||
|
|
||||||
StartTime time.Time `gorm:"column(start_time)" json:"startTime"` // 开始时间: 2023-11-08 02:00:00
|
LastStatus TaskStatus // 最近一次执行状态
|
||||||
Interval time.Duration `gorm:"column(interval)" json:"interval"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
LastResult string // 最近一次执行结果
|
||||||
Enabled bool `gorm:"column(enabled)" json:"enabled"` // 是否启用
|
LastTime timex.NullTime // 最近一次执行时间
|
||||||
LastStatus TaskStatus `gorm:"column(last_status)" json:"lastStatus"` // 最近一次执行状态
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
LastResult string `gorm:"column(last_result)" json:"lastResult"` // 最近一次执行结果
|
|
||||||
LastTime time.Time `gorm:"column(last_time)" json:"lastTime"` // 最近一次执行时间: 2023-11-08 02:00:00
|
|
||||||
DbInstanceId uint64 `gorm:"column(db_instance_id)" json:"dbInstanceId"`
|
|
||||||
Deadline time.Time `gorm:"-" json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDbBinlog(history *DbBackupHistory) *DbBinlog {
|
func NewDbBinlog(instanceId uint64) *DbBinlog {
|
||||||
binlogTask := &DbBinlog{
|
binlogTask := &DbBinlog{}
|
||||||
StartTime: time.Now(),
|
binlogTask.Id = instanceId
|
||||||
Enabled: true,
|
binlogTask.DbInstanceId = instanceId
|
||||||
Interval: BinlogDownloadInterval,
|
|
||||||
DbInstanceId: history.DbInstanceId,
|
|
||||||
LastTime: time.Now(),
|
|
||||||
}
|
|
||||||
binlogTask.Id = binlogTask.DbInstanceId
|
|
||||||
return binlogTask
|
return binlogTask
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbBinlog) TableName() string {
|
// BinlogFile is the metadata of the MySQL binlog file.
|
||||||
return "t_db_binlog"
|
type BinlogFile struct {
|
||||||
}
|
Name string
|
||||||
|
Size int64
|
||||||
|
|
||||||
func (d *DbBinlog) GetId() uint64 {
|
// Sequence is parsed from Name and is for the sorting purpose.
|
||||||
if d == nil {
|
Sequence int64
|
||||||
return 0
|
FirstEventTime time.Time
|
||||||
}
|
Downloaded bool
|
||||||
return d.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBinlog) GetDeadline() time.Time {
|
|
||||||
return d.Deadline
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBinlog) Schedule() bool {
|
|
||||||
if !d.Enabled {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch d.LastStatus {
|
|
||||||
case TaskSuccess:
|
|
||||||
if d.Interval == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
lastTime := d.LastTime
|
|
||||||
if d.LastTime.Sub(d.StartTime) < 0 {
|
|
||||||
lastTime = d.StartTime.Add(-d.Interval)
|
|
||||||
}
|
|
||||||
d.Deadline = lastTime.Add(d.Interval - d.LastTime.Sub(d.StartTime)%d.Interval)
|
|
||||||
case TaskFailed:
|
|
||||||
d.Deadline = time.Now().Add(time.Minute)
|
|
||||||
default:
|
|
||||||
d.Deadline = d.StartTime
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBinlog) IsFinished() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbBinlog) Update(task DbTask) bool {
|
|
||||||
switch t := task.(type) {
|
|
||||||
case *DbBinlog:
|
|
||||||
d.StartTime = t.StartTime
|
|
||||||
d.Interval = t.Interval
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
"mayfly-go/internal/db/dbm"
|
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbInstance struct {
|
type DbInstance struct {
|
||||||
model.Model
|
model.Model
|
||||||
|
|
||||||
Name string `orm:"column(name)" json:"name"`
|
Name string `json:"name"`
|
||||||
Type dbm.DbType `orm:"column(type)" json:"type"` // 类型,mysql oracle等
|
Type string `json:"type"` // 类型,mysql oracle等
|
||||||
Host string `orm:"column(host)" json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `orm:"column(port)" json:"port"`
|
Port int `json:"port"`
|
||||||
Network string `orm:"column(network)" json:"network"`
|
Network string `json:"network"`
|
||||||
Username string `orm:"column(username)" json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `orm:"column(password)" json:"-"`
|
Password string `json:"-"`
|
||||||
Params string `orm:"column(params)" json:"params"`
|
Params string `json:"params"`
|
||||||
Remark string `orm:"column(remark)" json:"remark"`
|
Remark string `json:"remark"`
|
||||||
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
|
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbInstance) TableName() string {
|
func (d *DbInstance) TableName() string {
|
||||||
@@ -39,12 +39,22 @@ func (d *DbInstance) GetNetwork() string {
|
|||||||
return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
|
return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbInstance) PwdEncrypt() {
|
func (d *DbInstance) PwdEncrypt() error {
|
||||||
// 密码替换为加密后的密码
|
// 密码替换为加密后的密码
|
||||||
d.Password = utils.PwdAesEncrypt(d.Password)
|
password, err := utils.PwdAesEncrypt(d.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("加密数据库密码失败")
|
||||||
|
}
|
||||||
|
d.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbInstance) PwdDecrypt() {
|
func (d *DbInstance) PwdDecrypt() error {
|
||||||
// 密码替换为解密后的密码
|
// 密码替换为解密后的密码
|
||||||
d.Password = utils.PwdAesDecrypt(d.Password)
|
password, err := utils.PwdAesDecrypt(d.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("解密数据库密码失败")
|
||||||
|
}
|
||||||
|
d.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,36 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ DbTask = (*DbRestore)(nil)
|
var _ DbTask = (*DbRestore)(nil)
|
||||||
|
|
||||||
// DbRestore 数据库恢复任务
|
// DbRestore 数据库恢复任务
|
||||||
type DbRestore struct {
|
type DbRestore struct {
|
||||||
model.Model
|
*DbTaskBase
|
||||||
|
|
||||||
DbName string `gorm:"column(db_name)" json:"dbName"` // 数据库名
|
DbName string `json:"dbName"` // 数据库名
|
||||||
StartTime time.Time `gorm:"column(start_time)" json:"startTime"` // 开始时间
|
PointInTime timex.NullTime `json:"pointInTime"` // 指定数据库恢复的时间点
|
||||||
Interval time.Duration `gorm:"column(interval)" json:"interval"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
DbBackupId uint64 `json:"dbBackupId"` // 用于恢复的数据库恢复任务ID
|
||||||
Enabled bool `gorm:"column(enabled)" json:"enabled"` // 是否启用
|
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 用于恢复的数据库恢复历史ID
|
||||||
Finished bool `gorm:"column(finished)" json:"finished"` // 是否完成
|
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称
|
||||||
Repeated bool `gorm:"column(repeated)" json:"repeated"` // 是否重复执行
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
LastStatus TaskStatus `gorm:"column(last_status)" json:"lastStatus"` // 最近一次执行状态
|
|
||||||
LastResult string `gorm:"column(last_result)" json:"lastResult"` // 最近一次执行结果
|
|
||||||
LastTime time.Time `gorm:"column(last_time)" json:"lastTime"` // 最近一次执行时间
|
|
||||||
PointInTime time.Time `gorm:"column(point_in_time)" json:"pointInTime"` // 指定数据库恢复的时间点
|
|
||||||
DbBackupId uint64 `gorm:"column(db_backup_id)" json:"dbBackupId"` // 用于恢复的数据库备份任务ID
|
|
||||||
DbBackupHistoryId uint64 `gorm:"column(db_backup_history_id)" json:"dbBackupHistoryId"` // 用于恢复的数据库备份历史ID
|
|
||||||
DbBackupHistoryName string `gorm:"column(db_backup_history_name) json:"dbBackupHistoryName"` // 数据库备份历史名称
|
|
||||||
DbInstanceId uint64 `gorm:"column(db_instance_id)" json:"dbInstanceId"`
|
|
||||||
Deadline time.Time `gorm:"-" json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbRestore) TableName() string {
|
func (*DbRestore) MessageWithStatus(status TaskStatus) string {
|
||||||
return "t_db_restore"
|
var result string
|
||||||
}
|
switch status {
|
||||||
|
case TaskDelay:
|
||||||
func (d *DbRestore) GetId() uint64 {
|
result = "等待恢复数据库"
|
||||||
if d == nil {
|
case TaskReady:
|
||||||
return 0
|
result = "准备恢复数据库"
|
||||||
}
|
case TaskReserved:
|
||||||
return d.Id
|
result = "数据库恢复中"
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbRestore) GetDeadline() time.Time {
|
|
||||||
return d.Deadline
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbRestore) Schedule() bool {
|
|
||||||
if d.Finished || !d.Enabled {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch d.LastStatus {
|
|
||||||
case TaskSuccess:
|
case TaskSuccess:
|
||||||
if d.Interval == 0 {
|
result = "数据库恢复成功"
|
||||||
return false
|
|
||||||
}
|
|
||||||
lastTime := d.LastTime
|
|
||||||
if d.LastTime.Sub(d.StartTime) < 0 {
|
|
||||||
lastTime = d.StartTime.Add(-d.Interval)
|
|
||||||
}
|
|
||||||
d.Deadline = lastTime.Add(d.Interval - d.LastTime.Sub(d.StartTime)%d.Interval)
|
|
||||||
case TaskFailed:
|
case TaskFailed:
|
||||||
d.Deadline = time.Now().Add(time.Minute)
|
result = "数据库恢复失败"
|
||||||
default:
|
|
||||||
d.Deadline = d.StartTime
|
|
||||||
}
|
}
|
||||||
return true
|
return result
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbRestore) IsFinished() bool {
|
|
||||||
return !d.Repeated && d.LastStatus == TaskSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbRestore) Update(task DbTask) bool {
|
|
||||||
switch backup := task.(type) {
|
|
||||||
case *DbRestore:
|
|
||||||
d.StartTime = backup.StartTime
|
|
||||||
d.Interval = backup.Interval
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
109
server/internal/db/domain/entity/db_task_base.go
Normal file
109
server/internal/db/domain/entity/db_task_base.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskDelay TaskStatus = iota
|
||||||
|
TaskReady
|
||||||
|
TaskReserved
|
||||||
|
TaskSuccess
|
||||||
|
TaskFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
const LastResultSize = 256
|
||||||
|
|
||||||
|
type DbTask interface {
|
||||||
|
model.ModelI
|
||||||
|
|
||||||
|
GetId() uint64
|
||||||
|
GetDeadline() time.Time
|
||||||
|
IsFinished() bool
|
||||||
|
Schedule() bool
|
||||||
|
Update(task DbTask)
|
||||||
|
GetTaskBase() *DbTaskBase
|
||||||
|
MessageWithStatus(status TaskStatus) string
|
||||||
|
IsEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDbBTaskBase(enabled bool, repeated bool, startTime time.Time, interval time.Duration) *DbTaskBase {
|
||||||
|
return &DbTaskBase{
|
||||||
|
Enabled: enabled,
|
||||||
|
Repeated: repeated,
|
||||||
|
StartTime: startTime,
|
||||||
|
Interval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbTaskBase struct {
|
||||||
|
model.Model
|
||||||
|
|
||||||
|
Enabled bool // 是否启用
|
||||||
|
StartTime time.Time // 开始时间
|
||||||
|
Interval time.Duration // 间隔时间
|
||||||
|
Repeated bool // 是否重复执行
|
||||||
|
LastStatus TaskStatus // 最近一次执行状态
|
||||||
|
LastResult string // 最近一次执行结果
|
||||||
|
LastTime timex.NullTime // 最近一次执行时间
|
||||||
|
Deadline time.Time `gorm:"-" json:"-"` // 计划执行时间
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) GetId() uint64 {
|
||||||
|
if d == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return d.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) GetDeadline() time.Time {
|
||||||
|
return d.Deadline
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) Schedule() bool {
|
||||||
|
if d.IsFinished() || !d.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch d.LastStatus {
|
||||||
|
case TaskSuccess:
|
||||||
|
if d.Interval == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lastTime := d.LastTime.Time
|
||||||
|
if lastTime.Sub(d.StartTime) < 0 {
|
||||||
|
lastTime = d.StartTime.Add(-d.Interval)
|
||||||
|
}
|
||||||
|
d.Deadline = lastTime.Add(d.Interval - lastTime.Sub(d.StartTime)%d.Interval)
|
||||||
|
case TaskFailed:
|
||||||
|
d.Deadline = time.Now().Add(time.Minute)
|
||||||
|
default:
|
||||||
|
d.Deadline = d.StartTime
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) IsFinished() bool {
|
||||||
|
return !d.Repeated && d.LastStatus == TaskSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) Update(task DbTask) {
|
||||||
|
t := task.GetTaskBase()
|
||||||
|
d.StartTime = t.StartTime
|
||||||
|
d.Interval = t.Interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) GetTaskBase() *DbTaskBase {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DbTaskBase) MessageWithStatus(_ TaskStatus) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DbTaskBase) IsEnabled() bool {
|
||||||
|
return d.Enabled
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type TaskStatus int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TaskDelay TaskStatus = iota
|
|
||||||
TaskReady
|
|
||||||
TaskReserved
|
|
||||||
TaskSuccess
|
|
||||||
TaskFailed
|
|
||||||
)
|
|
||||||
|
|
||||||
type DbTask interface {
|
|
||||||
GetId() uint64
|
|
||||||
GetDeadline() time.Time
|
|
||||||
IsFinished() bool
|
|
||||||
Schedule() bool
|
|
||||||
Update(task DbTask) bool
|
|
||||||
}
|
|
||||||
@@ -3,17 +3,14 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/pkg/base"
|
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbBackup interface {
|
type DbBackup interface {
|
||||||
base.Repo[*entity.DbBackup]
|
DbTask[*entity.DbBackup]
|
||||||
|
|
||||||
// GetDbBackupList 分页获取数据信息列表
|
// GetDbBackupList 分页获取数据信息列表
|
||||||
GetDbBackupList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
GetDbBackupList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||||
AddTask(ctx context.Context, tasks ...*entity.DbBackup) error
|
AddTask(ctx context.Context, tasks ...*entity.DbBackup) error
|
||||||
UpdateTaskStatus(ctx context.Context, task *entity.DbBackup) error
|
|
||||||
GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
|
GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
|
||||||
UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,4 @@ type DbBinlog interface {
|
|||||||
base.Repo[*entity.DbBinlog]
|
base.Repo[*entity.DbBinlog]
|
||||||
|
|
||||||
AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error
|
AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error
|
||||||
UpdateTaskStatus(ctx context.Context, task *entity.DbBinlog) error
|
|
||||||
UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ type DbBinlogHistory interface {
|
|||||||
GetHistories(instanceId uint64, start, target *entity.BinlogInfo) ([]*entity.DbBinlogHistory, error)
|
GetHistories(instanceId uint64, start, target *entity.BinlogInfo) ([]*entity.DbBinlogHistory, error)
|
||||||
GetHistoryByTime(instanceId uint64, targetTime time.Time) (*entity.DbBinlogHistory, error)
|
GetHistoryByTime(instanceId uint64, targetTime time.Time) (*entity.DbBinlogHistory, error)
|
||||||
GetLatestHistory(instanceId uint64) (*entity.DbBinlogHistory, bool, error)
|
GetLatestHistory(instanceId uint64) (*entity.DbBinlogHistory, bool, error)
|
||||||
|
InsertWithBinlogFiles(ctx context.Context, instanceId uint64, binlogFiles []*entity.BinlogFile) error
|
||||||
Upsert(ctx context.Context, history *entity.DbBinlogHistory) error
|
Upsert(ctx context.Context, history *entity.DbBinlogHistory) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/pkg/base"
|
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbRestore interface {
|
type DbRestore interface {
|
||||||
base.Repo[*entity.DbRestore]
|
DbTask[*entity.DbRestore]
|
||||||
|
|
||||||
// GetDbRestoreList 分页获取数据信息列表
|
// GetDbRestoreList 分页获取数据信息列表
|
||||||
GetDbRestoreList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
GetDbRestoreList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||||
AddTask(ctx context.Context, tasks ...*entity.DbRestore) error
|
AddTask(ctx context.Context, tasks ...*entity.DbRestore) error
|
||||||
UpdateTaskStatus(ctx context.Context, task *entity.DbRestore) error
|
|
||||||
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
|
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
|
||||||
UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
server/internal/db/domain/repository/db_task.go
Normal file
17
server/internal/db/domain/repository/db_task.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DbTask[T model.ModelI] interface {
|
||||||
|
base.Repo[T]
|
||||||
|
|
||||||
|
UpdateTaskStatus(ctx context.Context, task T) error
|
||||||
|
UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error
|
||||||
|
ListToDo() ([]T, error)
|
||||||
|
ListRepeating() ([]T, error)
|
||||||
|
AddTask(ctx context.Context, tasks ...T) error
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DbBackupSvc interface {
|
|
||||||
AddTask(ctx context.Context, tasks ...*entity.DbBackup) error
|
|
||||||
UpdateTask(ctx context.Context, task *entity.DbBackup) error
|
|
||||||
DeleteTask(ctx context.Context, taskId uint64) error
|
|
||||||
EnableTask(ctx context.Context, taskId uint64) error
|
|
||||||
DisableTask(ctx context.Context, taskId uint64) error
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DbBinlogSvc interface {
|
|
||||||
AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error
|
|
||||||
UpdateTask(ctx context.Context, task *entity.DbBinlog) error
|
|
||||||
DeleteTask(ctx context.Context, taskId uint64) error
|
|
||||||
EnableTask(ctx context.Context, taskId uint64) error
|
|
||||||
DisableTask(ctx context.Context, taskId uint64) error
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DbInstanceSvc interface {
|
|
||||||
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
|
||||||
Restore(ctx context.Context, task *entity.DbRestore) error
|
|
||||||
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool) error
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DbRestoreSvc interface {
|
|
||||||
AddTask(ctx context.Context, tasks ...*entity.DbRestore) error
|
|
||||||
UpdateTask(ctx context.Context, task *entity.DbRestore) error
|
|
||||||
DeleteTask(ctx context.Context, taskId uint64) error
|
|
||||||
EnableTask(ctx context.Context, taskId uint64) error
|
|
||||||
DisableTask(ctx context.Context, taskId uint64) error
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
|
||||||
"mayfly-go/pkg/gormx"
|
"mayfly-go/pkg/gormx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -16,7 +15,8 @@ import (
|
|||||||
var _ repository.DbBackup = (*dbBackupRepoImpl)(nil)
|
var _ repository.DbBackup = (*dbBackupRepoImpl)(nil)
|
||||||
|
|
||||||
type dbBackupRepoImpl struct {
|
type dbBackupRepoImpl struct {
|
||||||
base.RepoImpl[*entity.DbBackup]
|
//base.RepoImpl[*entity.DbBackup]
|
||||||
|
dbTaskBase[*entity.DbBackup]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDbBackupRepo() repository.DbBackup {
|
func NewDbBackupRepo() repository.DbBackup {
|
||||||
@@ -34,30 +34,6 @@ func (d *dbBackupRepoImpl) GetDbBackupList(condition *entity.DbBackupQuery, page
|
|||||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbBackupRepoImpl) UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error {
|
|
||||||
cond := map[string]any{
|
|
||||||
"id": taskId,
|
|
||||||
}
|
|
||||||
return d.Updates(cond, map[string]any{
|
|
||||||
"enabled": enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbBackupRepoImpl) UpdateTaskStatus(ctx context.Context, task *entity.DbBackup) error {
|
|
||||||
task = &entity.DbBackup{
|
|
||||||
Model: model.Model{
|
|
||||||
DeletedModel: model.DeletedModel{
|
|
||||||
Id: task.Id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Finished: task.Finished,
|
|
||||||
LastStatus: task.LastStatus,
|
|
||||||
LastResult: task.LastResult,
|
|
||||||
LastTime: task.LastTime,
|
|
||||||
}
|
|
||||||
return d.UpdateById(ctx, task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbBackupRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbBackup) error {
|
func (d *dbBackupRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbBackup) error {
|
||||||
return gormx.Tx(func(db *gorm.DB) error {
|
return gormx.Tx(func(db *gorm.DB) error {
|
||||||
var instanceId uint64
|
var instanceId uint64
|
||||||
@@ -94,7 +70,7 @@ func (d *dbBackupRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbBacku
|
|||||||
|
|
||||||
func (d *dbBackupRepoImpl) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
|
func (d *dbBackupRepoImpl) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
|
||||||
var dbNamesWithBackup []string
|
var dbNamesWithBackup []string
|
||||||
query := gormx.NewQuery(d.M).
|
query := gormx.NewQuery(d.GetModel()).
|
||||||
Eq("db_instance_id", instanceId).
|
Eq("db_instance_id", instanceId).
|
||||||
Eq("repeated", true)
|
Eq("repeated", true)
|
||||||
if err := query.GenGdb().Pluck("db_name", &dbNamesWithBackup).Error; err != nil {
|
if err := query.GenGdb().Pluck("db_name", &dbNamesWithBackup).Error; err != nil {
|
||||||
|
|||||||
@@ -57,5 +57,5 @@ func (repo *dbBackupHistoryRepoImpl) GetEarliestHistory(instanceId uint64) (*ent
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return history, err
|
return history, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package persistence
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
"mayfly-go/pkg/base"
|
||||||
"mayfly-go/pkg/global"
|
"mayfly-go/pkg/global"
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ repository.DbBinlog = (*dbBinlogRepoImpl)(nil)
|
var _ repository.DbBinlog = (*dbBinlogRepoImpl)(nil)
|
||||||
@@ -20,29 +20,9 @@ func NewDbBinlogRepo() repository.DbBinlog {
|
|||||||
return &dbBinlogRepoImpl{}
|
return &dbBinlogRepoImpl{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbBinlogRepoImpl) UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error {
|
func (d *dbBinlogRepoImpl) AddTaskIfNotExists(_ context.Context, task *entity.DbBinlog) error {
|
||||||
cond := map[string]any{
|
if err := global.Db.Clauses(clause.OnConflict{DoNothing: true}).Create(task).Error; err != nil {
|
||||||
"id": taskId,
|
return fmt.Errorf("启动 binlog 下载失败: %w", err)
|
||||||
}
|
}
|
||||||
return d.Updates(cond, map[string]any{
|
return nil
|
||||||
"enabled": enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbBinlogRepoImpl) UpdateTaskStatus(ctx context.Context, task *entity.DbBinlog) error {
|
|
||||||
task = &entity.DbBinlog{
|
|
||||||
Model: model.Model{
|
|
||||||
DeletedModel: model.DeletedModel{
|
|
||||||
Id: task.Id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LastStatus: task.LastStatus,
|
|
||||||
LastResult: task.LastResult,
|
|
||||||
LastTime: task.LastTime,
|
|
||||||
}
|
|
||||||
return d.UpdateById(ctx, task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbBinlogRepoImpl) AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error {
|
|
||||||
return global.Db.Clauses(clause.OnConflict{DoNothing: true}).Create(task).Error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ func (repo *dbBinlogHistoryRepoImpl) GetHistories(instanceId uint64, start, targ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *dbBinlogHistoryRepoImpl) GetLatestHistory(instanceId uint64) (*entity.DbBinlogHistory, bool, error) {
|
func (repo *dbBinlogHistoryRepoImpl) GetLatestHistory(instanceId uint64) (*entity.DbBinlogHistory, bool, error) {
|
||||||
gdb := gormx.NewQuery(repo.GetModel()).
|
history := &entity.DbBinlogHistory{}
|
||||||
|
err := gormx.NewQuery(repo.GetModel()).
|
||||||
Eq("db_instance_id", instanceId).
|
Eq("db_instance_id", instanceId).
|
||||||
Undeleted().
|
Undeleted().
|
||||||
OrderByDesc("sequence").
|
OrderByDesc("sequence").
|
||||||
GenGdb()
|
GenGdb().
|
||||||
history := &entity.DbBinlogHistory{}
|
First(history).Error
|
||||||
|
switch {
|
||||||
switch err := gdb.First(history).Error; {
|
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return history, true, nil
|
return history, true, nil
|
||||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
@@ -89,3 +89,35 @@ func (repo *dbBinlogHistoryRepoImpl) Upsert(_ context.Context, history *entity.D
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *dbBinlogHistoryRepoImpl) InsertWithBinlogFiles(ctx context.Context, instanceId uint64, binlogFiles []*entity.BinlogFile) error {
|
||||||
|
if len(binlogFiles) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
histories := make([]*entity.DbBinlogHistory, 0, len(binlogFiles))
|
||||||
|
for _, fileOnServer := range binlogFiles {
|
||||||
|
if !fileOnServer.Downloaded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
history := &entity.DbBinlogHistory{
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
FileName: fileOnServer.Name,
|
||||||
|
FileSize: fileOnServer.Size,
|
||||||
|
Sequence: fileOnServer.Sequence,
|
||||||
|
FirstEventTime: fileOnServer.FirstEventTime,
|
||||||
|
DbInstanceId: instanceId,
|
||||||
|
}
|
||||||
|
histories = append(histories, history)
|
||||||
|
}
|
||||||
|
if len(histories) > 1 {
|
||||||
|
if err := repo.BatchInsert(ctx, histories[:len(histories)-1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(histories) > 0 {
|
||||||
|
if err := repo.Upsert(ctx, histories[len(histories)-1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
|
||||||
"mayfly-go/pkg/gormx"
|
"mayfly-go/pkg/gormx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -16,7 +15,7 @@ import (
|
|||||||
var _ repository.DbRestore = (*dbRestoreRepoImpl)(nil)
|
var _ repository.DbRestore = (*dbRestoreRepoImpl)(nil)
|
||||||
|
|
||||||
type dbRestoreRepoImpl struct {
|
type dbRestoreRepoImpl struct {
|
||||||
base.RepoImpl[*entity.DbRestore]
|
dbTaskBase[*entity.DbRestore]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDbRestoreRepo() repository.DbRestore {
|
func NewDbRestoreRepo() repository.DbRestore {
|
||||||
@@ -34,21 +33,6 @@ func (d *dbRestoreRepoImpl) GetDbRestoreList(condition *entity.DbRestoreQuery, p
|
|||||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbRestoreRepoImpl) UpdateTaskStatus(ctx context.Context, task *entity.DbRestore) error {
|
|
||||||
task = &entity.DbRestore{
|
|
||||||
Model: model.Model{
|
|
||||||
DeletedModel: model.DeletedModel{
|
|
||||||
Id: task.Id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Finished: task.Finished,
|
|
||||||
LastStatus: task.LastStatus,
|
|
||||||
LastResult: task.LastResult,
|
|
||||||
LastTime: task.LastTime,
|
|
||||||
}
|
|
||||||
return d.UpdateById(ctx, task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbRestoreRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbRestore) error {
|
func (d *dbRestoreRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbRestore) error {
|
||||||
return gormx.Tx(func(db *gorm.DB) error {
|
return gormx.Tx(func(db *gorm.DB) error {
|
||||||
var instanceId uint64
|
var instanceId uint64
|
||||||
@@ -85,7 +69,7 @@ func (d *dbRestoreRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbRest
|
|||||||
|
|
||||||
func (d *dbRestoreRepoImpl) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
func (d *dbRestoreRepoImpl) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||||
var dbNamesWithRestore []string
|
var dbNamesWithRestore []string
|
||||||
query := gormx.NewQuery(d.M).
|
query := gormx.NewQuery(d.GetModel()).
|
||||||
Eq("db_instance_id", instanceId).
|
Eq("db_instance_id", instanceId).
|
||||||
Eq("repeated", true)
|
Eq("repeated", true)
|
||||||
if err := query.GenGdb().Pluck("db_name", &dbNamesWithRestore).Error; err != nil {
|
if err := query.GenGdb().Pluck("db_name", &dbNamesWithRestore).Error; err != nil {
|
||||||
@@ -99,12 +83,3 @@ func (d *dbRestoreRepoImpl) GetDbNamesWithoutRestore(instanceId uint64, dbNames
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbRestoreRepoImpl) UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error {
|
|
||||||
cond := map[string]any{
|
|
||||||
"id": taskId,
|
|
||||||
}
|
|
||||||
return d.Updates(cond, map[string]any{
|
|
||||||
"enabled": enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/internal/db/domain/entity"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/global"
|
||||||
|
"mayfly-go/pkg/gormx"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbTaskBase[T model.ModelI] struct {
|
||||||
|
base.RepoImpl[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbTaskBase[T]) UpdateEnabled(_ context.Context, taskId uint64, enabled bool) error {
|
||||||
|
cond := map[string]any{
|
||||||
|
"id": taskId,
|
||||||
|
}
|
||||||
|
return d.Updates(cond, map[string]any{
|
||||||
|
"enabled": enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbTaskBase[T]) UpdateTaskStatus(ctx context.Context, task T) error {
|
||||||
|
return d.UpdateById(ctx, task, "last_status", "last_result", "last_time")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbTaskBase[T]) ListToDo() ([]T, error) {
|
||||||
|
var tasks []T
|
||||||
|
db := global.Db.Model(d.GetModel())
|
||||||
|
err := db.Where("enabled = ?", true).
|
||||||
|
Where(db.Where("repeated = ?", true).Or("last_status <> ?", entity.TaskSuccess)).
|
||||||
|
Scopes(gormx.UndeleteScope).
|
||||||
|
Find(&tasks).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbTaskBase[T]) ListRepeating() ([]T, error) {
|
||||||
|
cond := map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"repeated": true,
|
||||||
|
}
|
||||||
|
var tasks []T
|
||||||
|
if err := d.ListByCond(cond, &tasks); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ service.DbBackupSvc = (*DbBackupSvcImpl)(nil)
|
|
||||||
|
|
||||||
type DbBackupSvcImpl struct {
|
|
||||||
repo repository.DbBackup
|
|
||||||
instanceRepo repository.Instance
|
|
||||||
scheduler *Scheduler[*entity.DbBackup]
|
|
||||||
binlogSvc service.DbBinlogSvc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIncUUID() (uuid.UUID, error) {
|
|
||||||
var uid uuid.UUID
|
|
||||||
now, seq, err := uuid.GetTime()
|
|
||||||
if err != nil {
|
|
||||||
return uid, err
|
|
||||||
}
|
|
||||||
timeHi := uint32((now >> 28) & 0xffffffff)
|
|
||||||
timeMid := uint16((now >> 12) & 0xffff)
|
|
||||||
timeLow := uint16(now & 0x0fff)
|
|
||||||
timeLow |= 0x1000 // Version 1
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(uid[0:], timeHi)
|
|
||||||
binary.BigEndian.PutUint16(uid[4:], timeMid)
|
|
||||||
binary.BigEndian.PutUint16(uid[6:], timeLow)
|
|
||||||
binary.BigEndian.PutUint16(uid[8:], seq)
|
|
||||||
|
|
||||||
copy(uid[10:], uuid.NodeID())
|
|
||||||
|
|
||||||
return uid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func withRunBackupTask(repositories *repository.Repositories, binlogSvc service.DbBinlogSvc) SchedulerOption[*entity.DbBackup] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbBackup]) {
|
|
||||||
scheduler.RunTask = func(ctx context.Context, task *entity.DbBackup) error {
|
|
||||||
instance := new(entity.DbInstance)
|
|
||||||
if err := repositories.Instance.GetById(instance, task.DbInstanceId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
instance.PwdDecrypt()
|
|
||||||
id, err := NewIncUUID()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
history := &entity.DbBackupHistory{
|
|
||||||
Uuid: id.String(),
|
|
||||||
DbBackupId: task.Id,
|
|
||||||
DbInstanceId: task.DbInstanceId,
|
|
||||||
DbName: task.DbName,
|
|
||||||
}
|
|
||||||
binlogInfo, err := NewDbInstanceSvc(instance, repositories).Backup(ctx, history)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
name := task.Name
|
|
||||||
if len(name) == 0 {
|
|
||||||
name = task.DbName
|
|
||||||
}
|
|
||||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
|
||||||
history.CreateTime = now
|
|
||||||
history.BinlogFileName = binlogInfo.FileName
|
|
||||||
history.BinlogSequence = binlogInfo.Sequence
|
|
||||||
history.BinlogPosition = binlogInfo.Position
|
|
||||||
|
|
||||||
if err := repositories.BackupHistory.Insert(ctx, history); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := binlogSvc.AddTaskIfNotExists(ctx, entity.NewDbBinlog(history)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
backupResult = map[entity.TaskStatus]string{
|
|
||||||
entity.TaskDelay: "等待备份数据库",
|
|
||||||
entity.TaskReady: "准备备份数据库",
|
|
||||||
entity.TaskReserved: "数据库备份中",
|
|
||||||
entity.TaskSuccess: "数据库备份成功",
|
|
||||||
entity.TaskFailed: "数据库备份失败",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func withUpdateBackupStatus(repositories *repository.Repositories) SchedulerOption[*entity.DbBackup] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbBackup]) {
|
|
||||||
scheduler.UpdateTaskStatus = func(ctx context.Context, status entity.TaskStatus, lastErr error, task *entity.DbBackup) error {
|
|
||||||
task.Finished = !task.Repeated && status == entity.TaskSuccess
|
|
||||||
task.LastStatus = status
|
|
||||||
var result = backupResult[status]
|
|
||||||
if lastErr != nil {
|
|
||||||
result = fmt.Sprintf("%v: %v", backupResult[status], lastErr)
|
|
||||||
}
|
|
||||||
task.LastResult = result
|
|
||||||
task.LastTime = time.Now()
|
|
||||||
return repositories.Backup.UpdateTaskStatus(ctx, task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDbBackupSvc(repositories *repository.Repositories, binlogSvc service.DbBinlogSvc) (service.DbBackupSvc, error) {
|
|
||||||
scheduler, err := NewScheduler[*entity.DbBackup](
|
|
||||||
withRunBackupTask(repositories, binlogSvc),
|
|
||||||
withUpdateBackupStatus(repositories))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
svc := &DbBackupSvcImpl{
|
|
||||||
repo: repositories.Backup,
|
|
||||||
instanceRepo: repositories.Instance,
|
|
||||||
scheduler: scheduler,
|
|
||||||
binlogSvc: binlogSvc,
|
|
||||||
}
|
|
||||||
err = svc.loadTasks(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) loadTasks(ctx context.Context) error {
|
|
||||||
tasks := make([]*entity.DbBackup, 0, 64)
|
|
||||||
cond := map[string]any{
|
|
||||||
"Enabled": true,
|
|
||||||
"Finished": false,
|
|
||||||
}
|
|
||||||
if err := svc.repo.ListByCond(cond, &tasks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, task := range tasks {
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) AddTask(ctx context.Context, tasks ...*entity.DbBackup) error {
|
|
||||||
for _, task := range tasks {
|
|
||||||
if err := svc.repo.AddTask(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) UpdateTask(ctx context.Context, task *entity.DbBackup) error {
|
|
||||||
if err := svc.repo.UpdateById(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) DeleteTask(ctx context.Context, taskId uint64) error {
|
|
||||||
// todo: 删除数据库备份历史文件
|
|
||||||
task := new(entity.DbBackup)
|
|
||||||
if err := svc.repo.GetById(task, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := svc.binlogSvc.DeleteTask(ctx, task.DbInstanceId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := svc.repo.DeleteById(ctx, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) EnableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task := new(entity.DbBackup)
|
|
||||||
if err := svc.repo.GetById(task, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBackupSvcImpl) DisableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task := new(entity.DbBackup)
|
|
||||||
if err := svc.repo.GetById(task, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageList 分页获取数据库备份任务
|
|
||||||
func (svc *DbBackupSvcImpl) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
|
||||||
return svc.repo.GetDbBackupList(condition, pageParam, toEntity, orderBy...)
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ service.DbBinlogSvc = (*DbBinlogSvcImpl)(nil)
|
|
||||||
|
|
||||||
type DbBinlogSvcImpl struct {
|
|
||||||
repo repository.DbBinlog
|
|
||||||
instanceRepo repository.Instance
|
|
||||||
scheduler *Scheduler[*entity.DbBinlog]
|
|
||||||
}
|
|
||||||
|
|
||||||
func withDownloadBinlog(repositories *repository.Repositories) SchedulerOption[*entity.DbBinlog] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbBinlog]) {
|
|
||||||
scheduler.RunTask = func(ctx context.Context, task *entity.DbBinlog) error {
|
|
||||||
instance := new(entity.DbInstance)
|
|
||||||
if err := repositories.Instance.GetById(instance, task.DbInstanceId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
instance.PwdDecrypt()
|
|
||||||
svc := NewDbInstanceSvc(instance, repositories)
|
|
||||||
err := svc.FetchBinlogs(ctx, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
binlogResult = map[entity.TaskStatus]string{
|
|
||||||
entity.TaskDelay: "等待备份BINLOG",
|
|
||||||
entity.TaskReady: "准备备份BINLOG",
|
|
||||||
entity.TaskReserved: "BINLOG备份中",
|
|
||||||
entity.TaskSuccess: "BINLOG备份成功",
|
|
||||||
entity.TaskFailed: "BINLOG备份失败",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func withUpdateBinlogStatus(repositories *repository.Repositories) SchedulerOption[*entity.DbBinlog] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbBinlog]) {
|
|
||||||
scheduler.UpdateTaskStatus = func(ctx context.Context, status entity.TaskStatus, lastErr error, task *entity.DbBinlog) error {
|
|
||||||
task.LastStatus = status
|
|
||||||
var result = backupResult[status]
|
|
||||||
if lastErr != nil {
|
|
||||||
result = fmt.Sprintf("%v: %v", binlogResult[status], lastErr)
|
|
||||||
}
|
|
||||||
task.LastResult = result
|
|
||||||
task.LastTime = time.Now()
|
|
||||||
return repositories.Binlog.UpdateTaskStatus(ctx, task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDbBinlogSvc(repositories *repository.Repositories) (service.DbBinlogSvc, error) {
|
|
||||||
scheduler, err := NewScheduler[*entity.DbBinlog](withDownloadBinlog(repositories), withUpdateBinlogStatus(repositories))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
svc := &DbBinlogSvcImpl{
|
|
||||||
repo: repositories.Binlog,
|
|
||||||
instanceRepo: repositories.Instance,
|
|
||||||
scheduler: scheduler,
|
|
||||||
}
|
|
||||||
err = svc.loadTasks(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) loadTasks(ctx context.Context) error {
|
|
||||||
tasks := make([]*entity.DbBinlog, 0, 64)
|
|
||||||
cond := map[string]any{
|
|
||||||
"Enabled": true,
|
|
||||||
}
|
|
||||||
if err := svc.repo.ListByCond(cond, &tasks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, task := range tasks {
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error {
|
|
||||||
if err := svc.repo.AddTaskIfNotExists(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if task.GetId() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) UpdateTask(ctx context.Context, task *entity.DbBinlog) error {
|
|
||||||
if err := svc.repo.UpdateById(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) DeleteTask(ctx context.Context, taskId uint64) error {
|
|
||||||
// todo: 删除 Binlog 历史文件
|
|
||||||
if err := svc.repo.DeleteById(ctx, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) EnableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task := new(entity.DbBinlog)
|
|
||||||
if err := svc.repo.GetById(task, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbBinlogSvcImpl) DisableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/internal/db/domain/service"
|
|
||||||
"mayfly-go/pkg/model"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ service.DbRestoreSvc = (*DbRestoreSvcImpl)(nil)
|
|
||||||
|
|
||||||
type DbRestoreSvcImpl struct {
|
|
||||||
repo repository.DbRestore
|
|
||||||
instanceRepo repository.Instance
|
|
||||||
scheduler *Scheduler[*entity.DbRestore]
|
|
||||||
}
|
|
||||||
|
|
||||||
func withRunRestoreTask(repositories *repository.Repositories) SchedulerOption[*entity.DbRestore] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbRestore]) {
|
|
||||||
scheduler.RunTask = func(ctx context.Context, task *entity.DbRestore) error {
|
|
||||||
instance := new(entity.DbInstance)
|
|
||||||
if err := repositories.Instance.GetById(instance, task.DbInstanceId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
instance.PwdDecrypt()
|
|
||||||
if err := NewDbInstanceSvc(instance, repositories).Restore(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
history := &entity.DbRestoreHistory{
|
|
||||||
CreateTime: time.Now(),
|
|
||||||
DbRestoreId: task.Id,
|
|
||||||
}
|
|
||||||
if err := repositories.RestoreHistory.Insert(ctx, history); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
restoreResult = map[entity.TaskStatus]string{
|
|
||||||
entity.TaskDelay: "等待恢复数据库",
|
|
||||||
entity.TaskReady: "准备恢复数据库",
|
|
||||||
entity.TaskReserved: "数据库恢复中",
|
|
||||||
entity.TaskSuccess: "数据库恢复成功",
|
|
||||||
entity.TaskFailed: "数据库恢复失败",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func withUpdateRestoreStatus(repositories *repository.Repositories) SchedulerOption[*entity.DbRestore] {
|
|
||||||
return func(scheduler *Scheduler[*entity.DbRestore]) {
|
|
||||||
scheduler.UpdateTaskStatus = func(ctx context.Context, status entity.TaskStatus, lastErr error, task *entity.DbRestore) error {
|
|
||||||
task.Finished = !task.Repeated && status == entity.TaskSuccess
|
|
||||||
task.LastStatus = status
|
|
||||||
var result = restoreResult[status]
|
|
||||||
if lastErr != nil {
|
|
||||||
result = fmt.Sprintf("%v: %v", restoreResult[status], lastErr)
|
|
||||||
}
|
|
||||||
task.LastResult = result
|
|
||||||
task.LastTime = time.Now()
|
|
||||||
return repositories.Restore.UpdateTaskStatus(ctx, task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDbRestoreSvc(repositories *repository.Repositories) (service.DbRestoreSvc, error) {
|
|
||||||
scheduler, err := NewScheduler[*entity.DbRestore](
|
|
||||||
withRunRestoreTask(repositories),
|
|
||||||
withUpdateRestoreStatus(repositories))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
svc := &DbRestoreSvcImpl{
|
|
||||||
repo: repositories.Restore,
|
|
||||||
instanceRepo: repositories.Instance,
|
|
||||||
scheduler: scheduler,
|
|
||||||
}
|
|
||||||
if err := svc.loadTasks(context.Background()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) loadTasks(ctx context.Context) error {
|
|
||||||
tasks := make([]*entity.DbRestore, 0, 64)
|
|
||||||
cond := map[string]any{
|
|
||||||
"Enabled": true,
|
|
||||||
"Finished": false,
|
|
||||||
}
|
|
||||||
if err := svc.repo.ListByCond(cond, &tasks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, task := range tasks {
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) AddTask(ctx context.Context, tasks ...*entity.DbRestore) error {
|
|
||||||
for _, task := range tasks {
|
|
||||||
if err := svc.repo.AddTask(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.PushTask(ctx, task)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) UpdateTask(ctx context.Context, task *entity.DbRestore) error {
|
|
||||||
if err := svc.repo.UpdateById(ctx, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) DeleteTask(ctx context.Context, taskId uint64) error {
|
|
||||||
// todo: 删除数据库恢复历史文件
|
|
||||||
if err := svc.repo.DeleteById(ctx, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) EnableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task := new(entity.DbRestore)
|
|
||||||
if err := svc.repo.GetById(task, taskId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.UpdateTask(ctx, task)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *DbRestoreSvcImpl) DisableTask(ctx context.Context, taskId uint64) error {
|
|
||||||
if err := svc.repo.UpdateEnabled(ctx, taskId, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.scheduler.RemoveTask(taskId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageList 分页获取数据库恢复任务
|
|
||||||
func (svc *DbRestoreSvcImpl) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
|
||||||
return svc.repo.GetDbRestoreList(condition, pageParam, toEntity, orderBy...)
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"mayfly-go/internal/db/domain/entity"
|
|
||||||
"mayfly-go/pkg/logx"
|
|
||||||
"mayfly-go/pkg/queue"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scheduler[T entity.DbTask] struct {
|
|
||||||
mutex sync.Mutex
|
|
||||||
wg sync.WaitGroup
|
|
||||||
queue *queue.DelayQueue[T]
|
|
||||||
closed bool
|
|
||||||
curTask T
|
|
||||||
curTaskContext context.Context
|
|
||||||
curTaskCancel context.CancelFunc
|
|
||||||
UpdateTaskStatus func(ctx context.Context, status entity.TaskStatus, lastErr error, task T) error
|
|
||||||
RunTask func(ctx context.Context, task T) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type SchedulerOption[T entity.DbTask] func(*Scheduler[T])
|
|
||||||
|
|
||||||
func NewScheduler[T entity.DbTask](opts ...SchedulerOption[T]) (*Scheduler[T], error) {
|
|
||||||
scheduler := &Scheduler[T]{
|
|
||||||
queue: queue.NewDelayQueue[T](0),
|
|
||||||
}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(scheduler)
|
|
||||||
}
|
|
||||||
if scheduler.RunTask == nil || scheduler.UpdateTaskStatus == nil {
|
|
||||||
return nil, errors.New("调度器没有设置 RunTask 或 UpdateTaskStatus")
|
|
||||||
}
|
|
||||||
scheduler.wg.Add(1)
|
|
||||||
go scheduler.run()
|
|
||||||
return scheduler, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) PushTask(ctx context.Context, task T) bool {
|
|
||||||
if !task.Schedule() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.queue.Enqueue(ctx, task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) UpdateTask(ctx context.Context, task T) bool {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
if task.GetId() == m.curTask.GetId() {
|
|
||||||
return m.curTask.Update(task)
|
|
||||||
}
|
|
||||||
oldTask, ok := m.queue.Remove(ctx, task.GetId())
|
|
||||||
if ok {
|
|
||||||
if !oldTask.Update(task) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
oldTask = task
|
|
||||||
}
|
|
||||||
if !oldTask.Schedule() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.queue.Enqueue(ctx, oldTask)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) updateCurTask(status entity.TaskStatus, lastErr error, task T) bool {
|
|
||||||
seconds := []time.Duration{time.Second * 1, time.Second * 8, time.Second * 64}
|
|
||||||
for _, second := range seconds {
|
|
||||||
if m.closed {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), second)
|
|
||||||
err := m.UpdateTaskStatus(ctx, status, lastErr, task)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
|
||||||
logx.Errorf("保存任务失败: %v", err)
|
|
||||||
time.Sleep(second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) run() {
|
|
||||||
defer m.wg.Done()
|
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
for !m.closed {
|
|
||||||
m.mutex.Lock()
|
|
||||||
ctx, cancel = context.WithTimeout(context.Background(), time.Millisecond)
|
|
||||||
task, ok := m.queue.Dequeue(ctx)
|
|
||||||
cancel()
|
|
||||||
if !ok {
|
|
||||||
m.mutex.Unlock()
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.curTask = task
|
|
||||||
m.updateCurTask(entity.TaskReserved, nil, task)
|
|
||||||
m.curTaskContext, m.curTaskCancel = context.WithCancel(context.Background())
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
err := m.RunTask(m.curTaskContext, task)
|
|
||||||
|
|
||||||
m.mutex.Lock()
|
|
||||||
taskStatus := entity.TaskSuccess
|
|
||||||
if err != nil {
|
|
||||||
taskStatus = entity.TaskFailed
|
|
||||||
}
|
|
||||||
m.updateCurTask(taskStatus, err, task)
|
|
||||||
m.cancelCurTask()
|
|
||||||
task.Schedule()
|
|
||||||
if !task.IsFinished() {
|
|
||||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
|
|
||||||
m.queue.Enqueue(ctx, task)
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
m.mutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) Close() {
|
|
||||||
if m.closed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mutex.Lock()
|
|
||||||
m.cancelCurTask()
|
|
||||||
m.closed = true
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) RemoveTask(taskId uint64) bool {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.queue.Remove(context.Background(), taskId)
|
|
||||||
if taskId == m.curTask.GetId() {
|
|
||||||
m.cancelCurTask()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Scheduler[T]) cancelCurTask() {
|
|
||||||
if m.curTaskCancel != nil {
|
|
||||||
m.curTaskCancel()
|
|
||||||
m.curTaskCancel = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,8 @@ func InitDbBackupRouter(router *gin.RouterGroup) {
|
|||||||
req.NewPut(":dbId/backups/:backupId/enable", d.Enable).Log(req.NewLogSave("db-启用数据库备份任务")),
|
req.NewPut(":dbId/backups/:backupId/enable", d.Enable).Log(req.NewLogSave("db-启用数据库备份任务")),
|
||||||
// 禁用数据库备份任务
|
// 禁用数据库备份任务
|
||||||
req.NewPut(":dbId/backups/:backupId/disable", d.Disable).Log(req.NewLogSave("db-禁用数据库备份任务")),
|
req.NewPut(":dbId/backups/:backupId/disable", d.Disable).Log(req.NewLogSave("db-禁用数据库备份任务")),
|
||||||
|
// 立即启动数据库备份任务
|
||||||
|
req.NewPut(":dbId/backups/:backupId/start", d.Start).Log(req.NewLogSave("db-立即启动数据库备份任务")),
|
||||||
// 删除数据库备份任务
|
// 删除数据库备份任务
|
||||||
req.NewDelete(":dbId/backups/:backupId", d.Delete),
|
req.NewDelete(":dbId/backups/:backupId", d.Delete),
|
||||||
// 获取未配置定时备份的数据库名称
|
// 获取未配置定时备份的数据库名称
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine, tagIds ..
|
|||||||
|
|
||||||
err := m.GetBy(oldMachine)
|
err := m.GetBy(oldMachine)
|
||||||
|
|
||||||
me.PwdEncrypt()
|
if errEnc := me.PwdEncrypt(); errEnc != nil {
|
||||||
|
return errorx.NewBiz(errEnc.Error())
|
||||||
|
}
|
||||||
if me.Id == 0 {
|
if me.Id == 0 {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return errorx.NewBiz("该机器信息已存在")
|
return errorx.NewBiz("该机器信息已存在")
|
||||||
@@ -242,13 +244,17 @@ func (m *machineAppImpl) toMachineInfo(me *entity.Machine) (*mcm.MachineInfo, er
|
|||||||
return nil, errorx.NewBiz("授权凭证信息已不存在,请重新关联")
|
return nil, errorx.NewBiz("授权凭证信息已不存在,请重新关联")
|
||||||
}
|
}
|
||||||
mi.AuthMethod = ac.AuthMethod
|
mi.AuthMethod = ac.AuthMethod
|
||||||
ac.PwdDecrypt()
|
if err := ac.PwdDecrypt(); err != nil {
|
||||||
|
return nil, errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
mi.Password = ac.Password
|
mi.Password = ac.Password
|
||||||
mi.Passphrase = ac.Passphrase
|
mi.Passphrase = ac.Passphrase
|
||||||
} else {
|
} else {
|
||||||
mi.AuthMethod = entity.AuthCertAuthMethodPassword
|
mi.AuthMethod = entity.AuthCertAuthMethodPassword
|
||||||
if me.Id != 0 {
|
if me.Id != 0 {
|
||||||
me.PwdDecrypt()
|
if err := me.PwdDecrypt(); err != nil {
|
||||||
|
return nil, errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mi.Password = me.Password
|
mi.Password = me.Password
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -16,7 +17,7 @@ type AuthCert struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthCert) TableName() string {
|
func (ac *AuthCert) TableName() string {
|
||||||
return "t_auth_cert"
|
return "t_auth_cert"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +29,32 @@ const (
|
|||||||
AuthCertTypePublic int8 = 2
|
AuthCertTypePublic int8 = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// 密码加密
|
// PwdEncrypt 密码加密
|
||||||
func (ac *AuthCert) PwdEncrypt() {
|
func (ac *AuthCert) PwdEncrypt() error {
|
||||||
ac.Password = utils.PwdAesEncrypt(ac.Password)
|
password, err := utils.PwdAesEncrypt(ac.Password)
|
||||||
ac.Passphrase = utils.PwdAesEncrypt(ac.Passphrase)
|
if err != nil {
|
||||||
|
return errors.New("加密授权凭证密码失败")
|
||||||
|
}
|
||||||
|
passphrase, err := utils.PwdAesEncrypt(ac.Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("加密授权凭证私钥失败")
|
||||||
|
}
|
||||||
|
ac.Password = password
|
||||||
|
ac.Passphrase = passphrase
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 密码解密
|
// PwdDecrypt 密码解密
|
||||||
func (ac *AuthCert) PwdDecrypt() {
|
func (ac *AuthCert) PwdDecrypt() error {
|
||||||
ac.Password = utils.PwdAesDecrypt(ac.Password)
|
password, err := utils.PwdAesDecrypt(ac.Password)
|
||||||
ac.Passphrase = utils.PwdAesDecrypt(ac.Passphrase)
|
if err != nil {
|
||||||
|
return errors.New("解密授权凭证密码失败")
|
||||||
|
}
|
||||||
|
passphrase, err := utils.PwdAesDecrypt(ac.Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("解密授权凭证私钥失败")
|
||||||
|
}
|
||||||
|
ac.Password = password
|
||||||
|
ac.Passphrase = passphrase
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -26,14 +27,24 @@ const (
|
|||||||
MachineStatusDisable int8 = -1 // 禁用状态
|
MachineStatusDisable int8 = -1 // 禁用状态
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Machine) PwdEncrypt() {
|
func (m *Machine) PwdEncrypt() error {
|
||||||
// 密码替换为加密后的密码
|
// 密码替换为加密后的密码
|
||||||
m.Password = utils.PwdAesEncrypt(m.Password)
|
password, err := utils.PwdAesEncrypt(m.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("加密主机密码失败")
|
||||||
|
}
|
||||||
|
m.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Machine) PwdDecrypt() {
|
func (m *Machine) PwdDecrypt() error {
|
||||||
// 密码替换为解密后的密码
|
// 密码替换为解密后的密码
|
||||||
m.Password = utils.PwdAesDecrypt(m.Password)
|
password, err := utils.PwdAesDecrypt(m.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("解密主机密码失败")
|
||||||
|
}
|
||||||
|
m.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Machine) UseAuthCert() bool {
|
func (m *Machine) UseAuthCert() bool {
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ func (r *Redis) GetRedisPwd(rc *req.Ctx) {
|
|||||||
rid := uint64(ginx.PathParamInt(rc.GinCtx, "id"))
|
rid := uint64(ginx.PathParamInt(rc.GinCtx, "id"))
|
||||||
re, err := r.RedisApp.GetById(new(entity.Redis), rid, "Password")
|
re, err := r.RedisApp.GetById(new(entity.Redis), rid, "Password")
|
||||||
biz.ErrIsNil(err, "redis信息不存在")
|
biz.ErrIsNil(err, "redis信息不存在")
|
||||||
re.PwdDecrypt()
|
if err := re.PwdDecrypt(); err != nil {
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
}
|
||||||
rc.ResData = re.Password
|
rc.ResData = re.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis, tagIds ...uin
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return errorx.NewBiz("该实例已存在")
|
return errorx.NewBiz("该实例已存在")
|
||||||
}
|
}
|
||||||
re.PwdEncrypt()
|
if errEnc := re.PwdEncrypt(); errEnc != nil {
|
||||||
|
return errorx.NewBiz(errEnc.Error())
|
||||||
|
}
|
||||||
|
|
||||||
resouceCode := stringx.Rand(16)
|
resouceCode := stringx.Rand(16)
|
||||||
re.Code = resouceCode
|
re.Code = resouceCode
|
||||||
@@ -108,7 +110,9 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis, tagIds ...uin
|
|||||||
oldRedis, _ = r.GetById(new(entity.Redis), re.Id)
|
oldRedis, _ = r.GetById(new(entity.Redis), re.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
re.PwdEncrypt()
|
if errEnc := re.PwdEncrypt(); errEnc != nil {
|
||||||
|
return errorx.NewBiz(errEnc.Error())
|
||||||
|
}
|
||||||
return r.Tx(ctx, func(ctx context.Context) error {
|
return r.Tx(ctx, func(ctx context.Context) error {
|
||||||
return r.UpdateById(ctx, re)
|
return r.UpdateById(ctx, re)
|
||||||
}, func(ctx context.Context) error {
|
}, func(ctx context.Context) error {
|
||||||
@@ -144,8 +148,9 @@ func (r *redisAppImpl) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("redis信息不存在")
|
return nil, errorx.NewBiz("redis信息不存在")
|
||||||
}
|
}
|
||||||
re.PwdDecrypt()
|
if err := re.PwdDecrypt(); err != nil {
|
||||||
|
return nil, errorx.NewBiz(err.Error())
|
||||||
|
}
|
||||||
return re.ToRedisInfo(db, r.tagApp.ListTagPathByResource(consts.TagResourceTypeRedis, re.Code)...), nil
|
return re.ToRedisInfo(db, r.tagApp.ListTagPathByResource(consts.TagResourceTypeRedis, re.Code)...), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
"mayfly-go/internal/redis/rdm"
|
"mayfly-go/internal/redis/rdm"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
@@ -21,20 +22,30 @@ type Redis struct {
|
|||||||
Remark string
|
Remark string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Redis) PwdEncrypt() {
|
func (r *Redis) PwdEncrypt() error {
|
||||||
// 密码替换为加密后的密码
|
// 密码替换为加密后的密码
|
||||||
r.Password = utils.PwdAesEncrypt(r.Password)
|
password, err := utils.PwdAesEncrypt(r.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("加密 Redis 密码失败")
|
||||||
|
}
|
||||||
|
r.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Redis) PwdDecrypt() {
|
func (r *Redis) PwdDecrypt() error {
|
||||||
// 密码替换为解密后的密码
|
// 密码替换为解密后的密码
|
||||||
r.Password = utils.PwdAesDecrypt(r.Password)
|
password, err := utils.PwdAesDecrypt(r.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("解密 Redis 密码失败")
|
||||||
|
}
|
||||||
|
r.Password = password
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为redisInfo进行连接
|
// ToRedisInfo 转换为redisInfo进行连接
|
||||||
func (re *Redis) ToRedisInfo(db int, tagPath ...string) *rdm.RedisInfo {
|
func (r *Redis) ToRedisInfo(db int, tagPath ...string) *rdm.RedisInfo {
|
||||||
redisInfo := new(rdm.RedisInfo)
|
redisInfo := new(rdm.RedisInfo)
|
||||||
structx.Copy(redisInfo, re)
|
_ = structx.Copy(redisInfo, r)
|
||||||
redisInfo.Db = db
|
redisInfo.Db = db
|
||||||
redisInfo.TagPath = tagPath
|
redisInfo.TagPath = tagPath
|
||||||
return redisInfo
|
return redisInfo
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,15 +28,25 @@ func (a *Account) IsEnable() bool {
|
|||||||
return a.Status == AccountEnableStatus
|
return a.Status == AccountEnableStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) OtpSecretEncrypt() {
|
func (a *Account) OtpSecretEncrypt() error {
|
||||||
a.OtpSecret = utils.PwdAesEncrypt(a.OtpSecret)
|
secret, err := utils.PwdAesEncrypt(a.OtpSecret)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("加密账户密码失败")
|
||||||
|
}
|
||||||
|
a.OtpSecret = secret
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) OtpSecretDecrypt() {
|
func (a *Account) OtpSecretDecrypt() error {
|
||||||
if a.OtpSecret == "-" {
|
if a.OtpSecret == "-" {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
a.OtpSecret = utils.PwdAesDecrypt(a.OtpSecret)
|
secret, err := utils.PwdAesDecrypt(a.OtpSecret)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("解密账户密码失败")
|
||||||
|
}
|
||||||
|
a.OtpSecret = secret
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
// 基础repo接口
|
// 基础repo接口
|
||||||
type Repo[T model.ModelI] interface {
|
type Repo[T model.ModelI] interface {
|
||||||
|
|
||||||
|
// GetModel 获取表的模型实例
|
||||||
|
GetModel() T
|
||||||
|
|
||||||
// 新增一个实体
|
// 新增一个实体
|
||||||
Insert(ctx context.Context, e T) error
|
Insert(ctx context.Context, e T) error
|
||||||
|
|
||||||
@@ -24,10 +27,10 @@ type Repo[T model.ModelI] interface {
|
|||||||
BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []T) error
|
BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []T) error
|
||||||
|
|
||||||
// 根据实体id更新实体信息
|
// 根据实体id更新实体信息
|
||||||
UpdateById(ctx context.Context, e T) error
|
UpdateById(ctx context.Context, e T, columns ...string) error
|
||||||
|
|
||||||
// 使用指定gorm db执行,主要用于事务执行
|
// 使用指定gorm db执行,主要用于事务执行
|
||||||
UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T) error
|
UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, columns ...string) error
|
||||||
|
|
||||||
// 根据实体主键删除实体
|
// 根据实体主键删除实体
|
||||||
DeleteById(ctx context.Context, id uint64) error
|
DeleteById(ctx context.Context, id uint64) error
|
||||||
@@ -101,16 +104,16 @@ func (br *RepoImpl[T]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []
|
|||||||
return gormx.BatchInsertWithDb(db, es)
|
return gormx.BatchInsertWithDb(db, es)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T) error {
|
func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T, columns ...string) error {
|
||||||
if db := contextx.GetDb(ctx); db != nil {
|
if db := contextx.GetDb(ctx); db != nil {
|
||||||
return br.UpdateByIdWithDb(ctx, db, e)
|
return br.UpdateByIdWithDb(ctx, db, e, columns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gormx.UpdateById(br.setBaseInfo(ctx, e))
|
return gormx.UpdateById(br.setBaseInfo(ctx, e), columns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *RepoImpl[T]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T) error {
|
func (br *RepoImpl[T]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, columns ...string) error {
|
||||||
return gormx.UpdateByIdWithDb(db, br.setBaseInfo(ctx, e))
|
return gormx.UpdateByIdWithDb(db, br.setBaseInfo(ctx, e), columns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *RepoImpl[T]) Updates(cond any, udpateFields map[string]any) error {
|
func (br *RepoImpl[T]) Updates(cond any, udpateFields map[string]any) error {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
func ErrIsNil(err error, msgAndParams ...any) {
|
func ErrIsNil(err error, msgAndParams ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(msgAndParams) == 0 {
|
if len(msgAndParams) == 0 {
|
||||||
panic(err)
|
panic(errorx.NewBiz(err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(errorx.NewBiz(fmt.Sprintf(msgAndParams[0].(string), msgAndParams[1:]...)))
|
panic(errorx.NewBiz(fmt.Sprintf(msgAndParams[0].(string), msgAndParams[1:]...)))
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ func (a *Aes) DecryptBase64(data string) ([]byte, error) {
|
|||||||
return cryptox.AesDecryptBase64(data, []byte(a.Key))
|
return cryptox.AesDecryptBase64(data, []byte(a.Key))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Aes) Valid() {
|
func (a *Aes) Valid() {
|
||||||
if j.Key == "" {
|
if a.Key == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
aesKeyLen := len(j.Key)
|
aesKeyLen := len(a.Key)
|
||||||
assert.IsTrue(aesKeyLen == 16 || aesKeyLen == 24 || aesKeyLen == 32,
|
assert.IsTrue(aesKeyLen == 16 || aesKeyLen == 24 || aesKeyLen == 32,
|
||||||
fmt.Sprintf("config.yml之 [aes.key] 长度需为16、24、32位长度, 当前为%d位", aesKeyLen))
|
fmt.Sprintf("config.yml之 [aes.key] 长度需为16、24、32位长度, 当前为%d位", aesKeyLen))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,12 +146,12 @@ func BatchInsertWithDb[T any](db *gorm.DB, models []T) error {
|
|||||||
|
|
||||||
// 根据id更新model,更新字段为model中不为空的值,即int类型不为0,ptr类型不为nil这类字段值
|
// 根据id更新model,更新字段为model中不为空的值,即int类型不为0,ptr类型不为nil这类字段值
|
||||||
// @param model 数据库映射实体模型
|
// @param model 数据库映射实体模型
|
||||||
func UpdateById(model any) error {
|
func UpdateById(model any, columns ...string) error {
|
||||||
return UpdateByIdWithDb(global.Db, model)
|
return UpdateByIdWithDb(global.Db, model, columns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateByIdWithDb(db *gorm.DB, model any) error {
|
func UpdateByIdWithDb(db *gorm.DB, model any, columns ...string) error {
|
||||||
return db.Model(model).Updates(model).Error
|
return db.Model(model).Select(columns).Updates(model).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据实体条件,更新参数udpateFields指定字段
|
// 根据实体条件,更新参数udpateFields指定字段
|
||||||
|
|||||||
@@ -42,6 +42,28 @@ func NewDelayQueue[T Delayable](cap int) *DelayQueue[T] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DelayQueue[T]) TryDequeue() (T, bool) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if elm, ok := s.priorityQueue.Peek(0); ok {
|
||||||
|
delay := elm.GetDeadline().Sub(time.Now())
|
||||||
|
if delay < minTimerDelay {
|
||||||
|
// 无需延迟,头部元素出队后直接返回
|
||||||
|
_, _ = s.dequeue()
|
||||||
|
return elm, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DelayQueue[T]) TryEnqueue(val T) bool {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
return s.enqueue(val)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DelayQueue[T]) Dequeue(ctx context.Context) (T, bool) {
|
func (s *DelayQueue[T]) Dequeue(ctx context.Context) (T, bool) {
|
||||||
// 出队锁:避免因重复获取队列头部同一元素降低性能
|
// 出队锁:避免因重复获取队列头部同一元素降低性能
|
||||||
select {
|
select {
|
||||||
@@ -64,7 +86,6 @@ func (s *DelayQueue[T]) Dequeue(ctx context.Context) (T, bool) {
|
|||||||
// 接收直接转发的不需要延迟的新元素
|
// 接收直接转发的不需要延迟的新元素
|
||||||
select {
|
select {
|
||||||
case elm := <-s.transferChan:
|
case elm := <-s.transferChan:
|
||||||
delete(s.elmMap, elm.GetId())
|
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
return elm, true
|
return elm, true
|
||||||
default:
|
default:
|
||||||
@@ -78,7 +99,6 @@ func (s *DelayQueue[T]) Dequeue(ctx context.Context) (T, bool) {
|
|||||||
if delay < minTimerDelay {
|
if delay < minTimerDelay {
|
||||||
// 无需延迟,头部元素出队后直接返回
|
// 无需延迟,头部元素出队后直接返回
|
||||||
_, _ = s.dequeue()
|
_, _ = s.dequeue()
|
||||||
delete(s.elmMap, elm.GetId())
|
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
return elm, ok
|
return elm, ok
|
||||||
}
|
}
|
||||||
@@ -122,6 +142,7 @@ func (s *DelayQueue[T]) dequeue() (T, bool) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return s.zero, false
|
return s.zero, false
|
||||||
}
|
}
|
||||||
|
delete(s.elmMap, elm.GetId())
|
||||||
select {
|
select {
|
||||||
case s.dequeuedSignal <- struct{}{}:
|
case s.dequeuedSignal <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -133,6 +154,7 @@ func (s *DelayQueue[T]) enqueue(val T) bool {
|
|||||||
if ok := s.priorityQueue.Enqueue(val); !ok {
|
if ok := s.priorityQueue.Enqueue(val); !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
s.elmMap[val.GetId()] = val
|
||||||
select {
|
select {
|
||||||
case s.enqueuedSignal <- struct{}{}:
|
case s.enqueuedSignal <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -156,7 +178,6 @@ func (s *DelayQueue[T]) Enqueue(ctx context.Context, val T) bool {
|
|||||||
|
|
||||||
// 如果队列未满,入队后直接返回
|
// 如果队列未满,入队后直接返回
|
||||||
if !s.priorityQueue.IsFull() {
|
if !s.priorityQueue.IsFull() {
|
||||||
s.elmMap[val.GetId()] = val
|
|
||||||
s.enqueue(val)
|
s.enqueue(val)
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"mayfly-go/pkg/validatorx"
|
"mayfly-go/pkg/validatorx"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ func RunWebServer() {
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
runnerWG := &sync.WaitGroup{}
|
|
||||||
|
|
||||||
// 初始化config.yml配置文件映射信息或使用环境变量。并初始化系统日志相关配置
|
// 初始化config.yml配置文件映射信息或使用环境变量。并初始化系统日志相关配置
|
||||||
config.Init()
|
config.Init()
|
||||||
|
|
||||||
@@ -52,6 +49,4 @@ func RunWebServer() {
|
|||||||
|
|
||||||
// 运行web服务
|
// 运行web服务
|
||||||
runWebServer(ctx)
|
runWebServer(ctx)
|
||||||
|
|
||||||
runnerWG.Wait()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"mayfly-go/initialize"
|
"mayfly-go/initialize"
|
||||||
|
"mayfly-go/internal/db/application"
|
||||||
"mayfly-go/pkg/config"
|
"mayfly-go/pkg/config"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
@@ -40,6 +41,8 @@ func runWebServer(ctx context.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("Failed to Shutdown HTTP Server: %v", err)
|
logx.Errorf("Failed to Shutdown HTTP Server: %v", err)
|
||||||
}
|
}
|
||||||
|
closeDbTasks()
|
||||||
|
// todo: close backupApp and restoreApp
|
||||||
}()
|
}()
|
||||||
|
|
||||||
confSrv := config.Conf.Server
|
confSrv := config.Conf.Server
|
||||||
@@ -56,3 +59,18 @@ func runWebServer(ctx context.Context) {
|
|||||||
logx.Errorf("Failed to Start HTTP Server: %v", err)
|
logx.Errorf("Failed to Start HTTP Server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeDbTasks() {
|
||||||
|
restoreApp := application.GetDbRestoreApp()
|
||||||
|
if restoreApp != nil {
|
||||||
|
restoreApp.Close()
|
||||||
|
}
|
||||||
|
binlogApp := application.GetDbBinlogApp()
|
||||||
|
if binlogApp != nil {
|
||||||
|
binlogApp.Close()
|
||||||
|
}
|
||||||
|
backupApp := application.GetDbBackupApp()
|
||||||
|
if backupApp != nil {
|
||||||
|
backupApp.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,3 +120,16 @@ func ToString(value any) string {
|
|||||||
return string(newValue)
|
return string(newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepZero 初始化对象
|
||||||
|
// 如 T 为基本类型或结构体,则返回零值
|
||||||
|
// 如 T 为指向基本类型或结构体的指针,则返回指向零值的指针
|
||||||
|
func DeepZero[T any]() T {
|
||||||
|
var data T
|
||||||
|
typ := reflect.TypeOf(data)
|
||||||
|
kind := typ.Kind()
|
||||||
|
if kind == reflect.Pointer {
|
||||||
|
return reflect.New(typ.Elem()).Interface().(T)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|||||||
14
server/pkg/utils/anyx/anyx_test.go
Normal file
14
server/pkg/utils/anyx/anyx_test.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package anyx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeepZero(t *testing.T) {
|
||||||
|
assert.Zero(t, DeepZero[int]())
|
||||||
|
assert.Zero(t, *DeepZero[*int]())
|
||||||
|
assert.Zero(t, DeepZero[time.Time]())
|
||||||
|
assert.Zero(t, *DeepZero[*time.Time]())
|
||||||
|
}
|
||||||
@@ -272,5 +272,9 @@ func pkcs7UnPadding(data []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
//获取填充的个数
|
//获取填充的个数
|
||||||
unPadding := int(data[length-1])
|
unPadding := int(data[length-1])
|
||||||
|
// todo fix: slice bounds out of range
|
||||||
|
if unPadding > length {
|
||||||
|
return nil, errors.New("解密字符串时去除填充个数超出字符串长度")
|
||||||
|
}
|
||||||
return data[:(length - unPadding)], nil
|
return data[:(length - unPadding)], nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,3 +115,17 @@ func ReverStrTemplate(temp, str string, res map[string]any) {
|
|||||||
ReverStrTemplate(next, Trim(SubString(str, UnicodeIndex(str, value)+Len(value), Len(str))), res)
|
ReverStrTemplate(next, Trim(SubString(str, UnicodeIndex(str, value)+Len(value), Len(str))), res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TruncateStr(s string, length int) string {
|
||||||
|
if length >= len(s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
var last int
|
||||||
|
for i := range s {
|
||||||
|
if i > length {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
last = i
|
||||||
|
}
|
||||||
|
return s[:last]
|
||||||
|
}
|
||||||
|
|||||||
32
server/pkg/utils/stringx/stringx_test.go
Normal file
32
server/pkg/utils/stringx/stringx_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package stringx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTruncateStr(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data string
|
||||||
|
length int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"123一二三", 0, ""},
|
||||||
|
{"123一二三", 1, "1"},
|
||||||
|
{"123一二三", 3, "123"},
|
||||||
|
{"123一二三", 4, "123"},
|
||||||
|
{"123一二三", 5, "123"},
|
||||||
|
{"123一二三", 6, "123一"},
|
||||||
|
{"123一二三", 7, "123一"},
|
||||||
|
{"123一二三", 11, "123一二"},
|
||||||
|
{"123一二三", 12, "123一二三"},
|
||||||
|
{"123一二三", 13, "123一二三"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(strconv.Itoa(tc.length), func(t *testing.T) {
|
||||||
|
got := TruncateStr(tc.data, tc.length)
|
||||||
|
require.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,60 @@
|
|||||||
package timex
|
package timex
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
const DefaultDateTimeFormat = "2006-01-02 15:04:05"
|
const DefaultDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
|
|
||||||
func DefaultFormat(time time.Time) string {
|
func DefaultFormat(time time.Time) string {
|
||||||
return time.Format(DefaultDateTimeFormat)
|
return time.Format(DefaultDateTimeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewNullTime(t time.Time) NullTime {
|
||||||
|
return NullTime{
|
||||||
|
NullTime: sql.NullTime{
|
||||||
|
Time: t,
|
||||||
|
Valid: !t.IsZero(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullTime struct {
|
||||||
|
sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nt *NullTime) UnmarshalJSON(bytes []byte) error {
|
||||||
|
if len(bytes) == 0 {
|
||||||
|
nt.NullTime = sql.NullTime{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var t time.Time
|
||||||
|
if err := json.Unmarshal(bytes, &t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.IsZero() {
|
||||||
|
nt.NullTime = sql.NullTime{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nt.NullTime = sql.NullTime{
|
||||||
|
Valid: true,
|
||||||
|
Time: t,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nt *NullTime) MarshalJSON() ([]byte, error) {
|
||||||
|
if !nt.Valid || nt.Time.IsZero() {
|
||||||
|
return json.Marshal(nil)
|
||||||
|
}
|
||||||
|
return json.Marshal(nt.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SleepWithContext(ctx context.Context, d time.Duration) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, d)
|
||||||
|
<-ctx.Done()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|||||||
77
server/pkg/utils/timex/timex_test.go
Normal file
77
server/pkg/utils/timex/timex_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package timex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNullTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
zero := time.Time{}
|
||||||
|
now := time.Now()
|
||||||
|
bytesNow, err := now.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want NullTime
|
||||||
|
bytes []byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
want: NewNullTime(zero),
|
||||||
|
bytes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "now",
|
||||||
|
want: NewNullTime(now),
|
||||||
|
bytes: bytesNow,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := &NullTime{}
|
||||||
|
err := got.UnmarshalJSON(tt.bytes)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.want.Valid, got.Valid)
|
||||||
|
require.True(t, got.Time.Equal(tt.want.Time))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullTime_MarshalJSON(t *testing.T) {
|
||||||
|
zero := time.Time{}
|
||||||
|
now := time.Now()
|
||||||
|
bytes, err := now.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nullTime NullTime
|
||||||
|
want []byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
nullTime: NewNullTime(zero),
|
||||||
|
want: []byte("null"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "now",
|
||||||
|
nullTime: NewNullTime(now),
|
||||||
|
want: bytes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.nullTime.MarshalJSON()
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -479,6 +479,8 @@ INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, crea
|
|||||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库查询最大结果集', 'DbQueryMaxCount', '[]', '200', '允许sql查询的最大结果集数。注: 0=不限制', '2023-02-11 14:29:03', 1, 'admin', '2023-02-11 14:40:56', 1, 'admin');
|
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库查询最大结果集', 'DbQueryMaxCount', '[]', '200', '允许sql查询的最大结果集数。注: 0=不限制', '2023-02-11 14:29:03', 1, 'admin', '2023-02-11 14:40:56', 1, 'admin');
|
||||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库是否记录查询SQL', 'DbSaveQuerySQL', '[]', '0', '1: 记录、0:不记录', '2023-02-11 16:07:14', 1, 'admin', '2023-02-11 16:44:17', 1, 'admin');
|
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库是否记录查询SQL', 'DbSaveQuerySQL', '[]', '0', '1: 记录、0:不记录', '2023-02-11 16:07:14', 1, 'admin', '2023-02-11 16:44:17', 1, 'admin');
|
||||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB\\\\2GB等)"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1GB"}', '机器相关配置,如终端回放路径等', 'admin,', '2023-07-13 16:26:44', 1, 'admin', '2023-11-09 22:01:31', 1, 'admin', 0, NULL);
|
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB\\\\2GB等)"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1GB"}', '机器相关配置,如终端回放路径等', 'admin,', '2023-07-13 16:26:44', 1, 'admin', '2023-11-09 22:01:31', 1, 'admin', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('Mysql可执行文件', 'MysqlBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":""}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"}]', '{"backupPath":"./db/backup"}', '', 'admin,', '2023-12-29 09:55:26', 1, 'admin', '2023-12-29 15:45:24', 1, 'admin', 0, NULL);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
@@ -865,7 +867,6 @@ CREATE TABLE `t_db_backup` (
|
|||||||
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
`finished` tinyint(1) DEFAULT NULL COMMENT '是否完成',
|
|
||||||
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次备份状态',
|
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次备份状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次备份结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次备份结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次备份时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次备份时间',
|
||||||
@@ -917,7 +918,6 @@ CREATE TABLE `t_db_restore` (
|
|||||||
`interval` bigint(20) DEFAULT NULL COMMENT '恢复周期',
|
`interval` bigint(20) DEFAULT NULL COMMENT '恢复周期',
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次恢复时间',
|
`start_time` datetime DEFAULT NULL COMMENT '首次恢复时间',
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
`finished` tinyint(1) DEFAULT NULL COMMENT '是否完成',
|
|
||||||
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次恢复状态',
|
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次恢复状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次恢复结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次恢复结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次恢复时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次恢复时间',
|
||||||
@@ -959,9 +959,6 @@ DROP TABLE IF EXISTS `t_db_binlog`;
|
|||||||
CREATE TABLE `t_db_binlog` (
|
CREATE TABLE `t_db_binlog` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`db_instance_id` bigint(20) unsigned NOT NULL COMMENT '数据库实例ID',
|
`db_instance_id` bigint(20) unsigned NOT NULL COMMENT '数据库实例ID',
|
||||||
`interval` bigint(20) DEFAULT NULL COMMENT '下载周期',
|
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次下载时间',
|
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '会否启用',
|
|
||||||
`last_status` bigint(20) DEFAULT NULL COMMENT '上次下载状态',
|
`last_status` bigint(20) DEFAULT NULL COMMENT '上次下载状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次下载结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次下载结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次下载时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次下载时间',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ CREATE TABLE `t_db_backup` (
|
|||||||
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
`finished` tinyint(1) DEFAULT NULL COMMENT '是否完成',
|
|
||||||
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次备份状态',
|
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次备份状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次备份结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次备份结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次备份时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次备份时间',
|
||||||
@@ -63,7 +62,6 @@ CREATE TABLE `t_db_restore` (
|
|||||||
`interval` bigint(20) DEFAULT NULL COMMENT '恢复周期',
|
`interval` bigint(20) DEFAULT NULL COMMENT '恢复周期',
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次恢复时间',
|
`start_time` datetime DEFAULT NULL COMMENT '首次恢复时间',
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
`finished` tinyint(1) DEFAULT NULL COMMENT '是否完成',
|
|
||||||
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次恢复状态',
|
`last_status` tinyint(4) DEFAULT NULL COMMENT '上次恢复状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次恢复结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次恢复结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次恢复时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次恢复时间',
|
||||||
@@ -105,9 +103,6 @@ DROP TABLE IF EXISTS `t_db_binlog`;
|
|||||||
CREATE TABLE `t_db_binlog` (
|
CREATE TABLE `t_db_binlog` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`db_instance_id` bigint(20) unsigned NOT NULL COMMENT '数据库实例ID',
|
`db_instance_id` bigint(20) unsigned NOT NULL COMMENT '数据库实例ID',
|
||||||
`interval` bigint(20) DEFAULT NULL COMMENT '下载周期',
|
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次下载时间',
|
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '会否启用',
|
|
||||||
`last_status` bigint(20) DEFAULT NULL COMMENT '上次下载状态',
|
`last_status` bigint(20) DEFAULT NULL COMMENT '上次下载状态',
|
||||||
`last_result` varchar(256) DEFAULT NULL COMMENT '上次下载结果',
|
`last_result` varchar(256) DEFAULT NULL COMMENT '上次下载结果',
|
||||||
`last_time` datetime DEFAULT NULL COMMENT '上次下载时间',
|
`last_time` datetime DEFAULT NULL COMMENT '上次下载时间',
|
||||||
@@ -140,3 +135,7 @@ CREATE TABLE `t_db_binlog_history` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_db_instance_id` (`db_instance_id`) USING BTREE
|
KEY `idx_db_instance_id` (`db_instance_id`) USING BTREE
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('Mysql可执行文件', 'MysqlBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":""}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"}]', '{"backupPath":"./db/backup"}', '', 'admin,', '2023-12-29 09:55:26', 1, 'admin', '2023-12-29 15:45:24', 1, 'admin', 0, NULL);
|
||||||
Reference in New Issue
Block a user