feat: MySQL backup supports custom parameters (#11575)

This commit is contained in:
ssongliu
2026-01-06 16:41:55 +08:00
committed by GitHub
parent f67bdf0c22
commit 6c94cb2bdf
24 changed files with 169 additions and 18 deletions
+7 -6
View File
@@ -65,12 +65,13 @@ type UploadForRecover struct {
}
type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Description string `json:"description"`
}
+3
View File
@@ -49,6 +49,7 @@ type CronjobOperate struct {
Timeout uint `json:"timeout" validate:"number,min=1"`
IgnoreErr bool `json:"ignoreErr"`
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
@@ -123,6 +124,7 @@ type CronjobInfo struct {
LastRecordTime string `json:"lastRecordTime"`
Status string `json:"status"`
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
}
@@ -162,6 +164,7 @@ type CronjobTrans struct {
IgnoreErr bool `json:"ignoreErr"`
SnapshotRule SnapshotTransHelper `json:"snapshotRule"`
Secret string `json:"secret"`
Args string `json:"args"`
SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"`
+1
View File
@@ -37,6 +37,7 @@ type Cronjob struct {
Timeout uint `json:"timeout"`
IgnoreErr bool `json:"ignoreErr"`
RetainCopies uint64 `json:"retainCopies"`
Args string `json:"args"`
IsExecuting bool `json:"isExecuting"`
Status string `json:"status"`
+8 -2
View File
@@ -44,7 +44,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
return err
}
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args}
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
@@ -87,7 +87,12 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
}
}
itemHandler := func() error { return doMysqlBackup(db, targetDir, fileName, secret) }
itemHandler := func() error {
if len(db.Args) != 0 {
backupTask.Logf("args: %v", db.Args)
}
return doMysqlBackup(db, targetDir, fileName, secret)
}
if parentTask != nil {
return itemHandler()
}
@@ -225,6 +230,7 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
Format: dbInfo.Format,
TargetDir: targetDir,
FileName: fileName,
Args: db.Args,
}
if err := cli.Backup(backupInfo); err != nil {
return err
+3
View File
@@ -143,6 +143,7 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
Timeout: cronjob.Timeout,
IgnoreErr: cronjob.IgnoreErr,
Secret: cronjob.Secret,
Args: cronjob.Args,
}
switch cronjob.Type {
case "app":
@@ -237,6 +238,7 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
Timeout: item.Timeout,
IgnoreErr: item.IgnoreErr,
Secret: item.Secret,
Args: item.Args,
}
hasNotFound := false
switch item.Type {
@@ -733,6 +735,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
upMap["timeout"] = req.Timeout
upMap["ignore_err"] = req.IgnoreErr
upMap["secret"] = req.Secret
upMap["args"] = req.Args
err = cronjobRepo.Update(id, upMap)
if err != nil {
return err
+5
View File
@@ -379,6 +379,7 @@ type DatabaseHelper struct {
DBType string
Database string
Name string
Args []string
}
func addSkipTask(source string, taskItem *task.Task) {
@@ -400,6 +401,7 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
DBType: cronjob.DBType,
Database: mysql.Database,
Name: mysql.Name,
Args: strings.Split(cronjob.Args, ","),
})
}
} else {
@@ -410,6 +412,7 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
DBType: cronjob.DBType,
Database: pg.PostgresqlName,
Name: pg.Name,
Args: strings.Split(cronjob.Args, ","),
})
}
}
@@ -425,6 +428,7 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
DBType: cronjob.DBType,
Database: mysqlItem.MysqlName,
Name: mysqlItem.Name,
Args: strings.Split(cronjob.Args, ","),
})
} else {
pgItem, _ := postgresqlRepo.Get(repo.WithByID(uint(itemID)))
@@ -433,6 +437,7 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
DBType: cronjob.DBType,
Database: pgItem.PostgresqlName,
Name: pgItem.Name,
Args: strings.Split(cronjob.Args, ","),
})
}
}
+1
View File
@@ -61,6 +61,7 @@ func InitAgentDB() {
migrations.AddisIPtoWebsiteSSL,
migrations.InitPingStatus,
migrations.UpdateApp,
migrations.AddCronjobArgs,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+7
View File
@@ -807,3 +807,10 @@ var UpdateApp = &gormigrate.Migration{
return tx.AutoMigrate(&model.App{})
},
}
var AddCronjobArgs = &gormigrate.Migration{
ID: "20260106-add-cronjob-args",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Cronjob{})
},
}
+7 -6
View File
@@ -72,12 +72,13 @@ type AccessChangeInfo struct {
}
type BackupInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Version string `json:"version"`
Format string `json:"format"`
TargetDir string `json:"targetDir"`
FileName string `json:"fileName"`
Name string `json:"name"`
Type string `json:"type"`
Version string `json:"version"`
Format string `json:"format"`
TargetDir string `json:"targetDir"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Timeout uint `json:"timeout"` // second
}
+10 -2
View File
@@ -237,9 +237,17 @@ func (r *Local) Backup(info BackupInfo) error {
if r.Type == constant.AppMariaDB {
dumpCmd = "mariadb-dump"
}
global.LOG.Infof("start to %s | gzip > %s.gzip", dumpCmd, info.TargetDir+"/"+info.FileName)
global.LOG.Infof("start to %s | gzip > %s.gzip, args: %v", dumpCmd, info.TargetDir+"/"+info.FileName, info.Args)
cmd := exec.Command("docker", "exec", r.ContainerName, dumpCmd, "--routines", "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
args := []string{"exec", r.ContainerName, dumpCmd, "--routines", "-uroot", "-p" + r.Password, "--default-character-set=" + info.Format}
for _, arg := range info.Args {
if len(arg) == 0 {
continue
}
args = append(args, arg)
}
args = append(args, info.Name)
cmd := exec.Command("docker", args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
+3
View File
@@ -67,6 +67,8 @@ export namespace Cronjob {
alertMethodItems: Array<string>;
scopes: string[];
args: string;
argItems: Array<string>;
}
export interface Item {
val: string;
@@ -113,6 +115,7 @@ export namespace Cronjob {
alertMethod: string;
scopes?: string[];
args: string;
}
export interface CronjobTrans {
name: string;
+15 -1
View File
@@ -100,7 +100,6 @@
<DialogPro
v-model="open"
:title="isBackup ? $t('commons.button.backup') : $t('commons.button.recover') + ' - ' + name"
size="small"
@close="handleBackupClose"
>
<el-alert :closable="false">
@@ -110,6 +109,17 @@
<el-form-item :label="$t('setting.compressPassword')">
<el-input v-model="secret" :placeholder="$t('setting.backupRecoverMessage')" />
</el-form-item>
<el-form-item v-if="type === 'mysql' || type === 'mysql-cluster'" :label="$t('cronjob.backupArgs')">
<el-select v-model="args" filterable allow-create multiple>
<el-option v-for="item in mysqlArgs" :key="item.arg" :value="item.arg" :label="item.arg">
{{ item.arg }}
<span class="ml-2">{{ item.description }}</span>
</el-option>
</el-select>
<span class="input-help">
{{ $t('cronjob.backupArgsHelper') }}
</span>
</el-form-item>
<el-form-item
v-if="!isBackup && type !== 'app' && type !== 'website'"
:label="$t('cronjob.timeout')"
@@ -166,6 +176,7 @@ import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import { routerToFileWithPath } from '@/utils/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { mysqlArgs } from '@/views/cronjob/cronjob/helper';
const { currentNode } = useGlobalStore();
const PushApp = defineAsyncComponent(async () => {
@@ -178,6 +189,7 @@ const PushApp = defineAsyncComponent(async () => {
});
const selects = ref<any>([]);
const args = ref([]);
const loading = ref();
const opRef = ref();
const taskLogRef = ref();
@@ -311,6 +323,7 @@ const backup = async () => {
secret: secret.value,
taskID: taskID,
description: description.value,
args: args.value,
};
loading.value = true;
await handleBackup(params, node.value)
@@ -352,6 +365,7 @@ const recover = async (row?: any) => {
const onBackup = async () => {
description.value = '';
secret.value = '';
args.value = [];
isBackup.value = true;
open.value = true;
};
+6
View File
@@ -1133,6 +1133,12 @@ const message = {
failedRecord: 'Failure records',
successRecord: 'Successful records',
database: 'Backup database',
backupArgs: 'Backup Arguments',
backupArgsHelper:
'Unlisted backup arguments can be manually entered and selected. For example: Enter --no-data and select the first option from the dropdown list.',
singleTransaction: 'Backup InnoDB tables using a single transaction, suitable for large-volume data backups',
quick: 'Read data row by row instead of loading the entire table into memory, suitable for large-volume data and low-memory machine backups',
skipLockTables: 'Backup without locking all tables, suitable for highly concurrent databases',
missBackupAccount: 'The backup account could not be found',
syncDate: 'Synchronization time ',
clean: 'Cache clean',
+8
View File
@@ -1137,6 +1137,14 @@ const message = {
failedRecord: 'Registros fallidos',
successRecord: 'Registros completados correctamente',
database: 'Respaldo de base de datos',
backupArgs: 'Argumentos de Copia de Seguridad',
backupArgsHelper:
'Los argumentos de copia de seguridad no listados pueden ser ingresados y seleccionados manualmente. Por ejemplo: Ingrese --no-data y seleccione la primera opción de la lista desplegable.',
singleTransaction:
'Copia de seguridad de tablas InnoDB utilizando una única transacción, adecuada para copias de seguridad de datos de gran volumen',
quick: 'Lee datos fila por fila en lugar de cargar la tabla completa en memoria, adecuado para copias de seguridad de datos de gran volumen y máquinas con poca memoria',
skipLockTables:
'Copia de seguridad sin bloquear todas las tablas, adecuada para bases de datos altamente concurrentes',
missBackupAccount: 'No se pudo encontrar la cuenta de respaldo',
syncDate: 'Hora de sincronización',
clean: 'Limpiar caché',
+7
View File
@@ -1105,6 +1105,13 @@ const message = {
failedRecord: '失敗記録',
successRecord: '成功した記録',
database: 'バックアップデータベース',
backupArgs: 'バックアップ引数',
backupArgsHelper:
'リストにないバックアップ引数は手動で入力して選択できます--no-data を入力しドロップダウンリストの最初のオプションを選択してください',
singleTransaction:
'単一トランザクションを使用して InnoDB テーブルをバックアップします大容量データのバックアップに適しています',
quick: 'テーブル全体をメモリにロードする代わりにデータを行単位で読み取ります大容量データや低メモリマシンのバックアップに適しています',
skipLockTables: 'すべてのテーブルをロックせずにバックアップします高並列データベースに適しています',
missBackupAccount: 'バックアップアカウントは見つかりませんでした',
syncDate: '同期時間',
clean: 'キャッシュクリーン',
+6
View File
@@ -1091,6 +1091,12 @@ const message = {
failedRecord: '실패한 레코드',
successRecord: '성공한 레코드',
database: '백업 데이터베이스',
backupArgs: '백업 인수',
backupArgsHelper:
'목록에 없는 백업 인수는 수동으로 입력하여 선택할 수 있습니다. 예: --no-data를 입력하고 드롭다운 목록의 첫 번째 옵션을 선택하세요.',
singleTransaction: '단일 트랜잭션을 사용하여 InnoDB 테이블을 백업하며, 대용량 데이터 백업에 적합합니다',
quick: '전체 테이블을 메모리에 로드하는 대신 데이터를 행별로 읽습니다. 대용량 데이터 및 저메모리 시스템 백업에 적합합니다',
skipLockTables: '모든 테이블을 잠그지 않고 백업합니다. 높은 동시성 데이터베이스에 적합합니다',
missBackupAccount: '백업 계정을 찾을 수 없습니다',
syncDate: '동기화 시간',
clean: '캐시 정리',
+7
View File
@@ -1125,6 +1125,13 @@ const message = {
failedRecord: 'Rekod kegagalan',
successRecord: 'Rekod berjaya',
database: 'Pangkalan data sandaran',
backupArgs: 'Argumen Sandaran',
backupArgsHelper:
'Argumen sandaran yang tidak disenaraikan boleh dimasukkan dan dipilih secara manual. Contoh: Masukkan --no-data dan pilih pilihan pertama dari senarai juntai bawah.',
singleTransaction:
'Sandaran jadual InnoDB menggunakan transaksi tunggal, sesuai untuk sandaran data isipadu besar',
quick: 'Baca data baris demi baris daripada memuatkan keseluruhan jadual ke dalam ingatan, sesuai untuk sandaran data isipadu besar dan mesin ingatan rendah',
skipLockTables: 'Sandaran tanpa mengunci semua jadual, sesuai untuk pangkalan data konkuren tinggi',
missBackupAccount: 'Akaun sandaran tidak dijumpai',
syncDate: 'Waktu penyelarasan',
clean: 'Bersihkan cache',
+7
View File
@@ -1122,6 +1122,13 @@ const message = {
failedRecord: 'Registros de falha',
successRecord: 'Registros de sucesso',
database: 'Backup de banco de dados',
backupArgs: 'Argumentos de Backup',
backupArgsHelper:
'Argumentos de backup não listados podem ser inseridos e selecionados manualmente. Por exemplo: Digite --no-data e selecione a primeira opção da lista suspensa.',
singleTransaction:
'Faz backup de tabelas InnoDB usando uma única transação, adequado para backups de dados de grande volume',
quick: ' dados linha por linha em vez de carregar toda a tabela na memória, adequado para backups de dados de grande volume e máquinas com pouca memória',
skipLockTables: 'Backup sem bloquear todas as tabelas, adequado para bancos de dados altamente concorrentes',
missBackupAccount: 'A conta de backup não foi encontrada',
syncDate: 'Data de sincronização',
clean: 'Limpeza de cache',
+7
View File
@@ -1118,6 +1118,13 @@ const message = {
failedRecord: 'Неудачные записи',
successRecord: 'Успешные записи',
database: 'Резервное копирование базы данных',
backupArgs: 'Аргументы Резервного Копирования',
backupArgsHelper:
'Неперечисленные аргументы резервного копирования могут быть введены и выбраны вручную. Например: Введите --no-data и выберите первый вариант из выпадающего списка.',
singleTransaction:
'Резервное копирование таблиц InnoDB с использованием одной транзакции, подходит для резервного копирования данных большого объема',
quick: 'Чтение данных построчно вместо загрузки всей таблицы в память, подходит для резервного копирования данных большого объема и машин с низкой памятью',
skipLockTables: 'Резервное копирование без блокировки всех таблиц, подходит для высококонкурентных баз данных',
missBackupAccount: 'Не удалось найти учетную запись резервного копирования',
syncDate: 'Время синхронизации',
clean: 'Очистка кэша',
+7
View File
@@ -1146,6 +1146,13 @@ const message = {
failedRecord: 'Başarısız kayıtlar',
successRecord: 'Başarılı kayıtlar',
database: 'Veritabanını yedekle',
backupArgs: 'Yedekleme Argümanları',
backupArgsHelper:
'Listelenmemiş yedekleme argümanları manuel olarak girilip seçilebilir. Örneğin: --no-data yazın ve açılır listeden ilk seçeneği seçin.',
singleTransaction:
'InnoDB tablolarını tek bir işlem kullanarak yedekler, büyük hacimli veri yedeklemeleri için uygundur',
quick: 'Tüm tabloyu belleğe yüklemek yerine verileri satır satır okur, büyük hacimli veri ve düşük bellekli makine yedeklemeleri için uygundur',
skipLockTables: 'Tüm tabloları kilitlemeden yedekleme, yüksek eşzamanlılığa sahip veritabanları için uygundur',
missBackupAccount: 'Yedekleme hesabı bulunamadı',
syncDate: 'Senkronizasyon zamanı ',
clean: 'Önbellek temizleme',
+5
View File
@@ -1072,6 +1072,11 @@ const message = {
rulesHelper: '支援多個排除規則使用英文逗號 , 分隔例如*.log,*.sql',
lastRecordTime: '上次執行情況',
database: '備份資料庫',
backupArgs: '備份參數',
backupArgsHelper: '未列出的備份參數可手動輸入並選擇例如輸入 --no-data 後選擇下拉列表中的第一個選項',
singleTransaction: '使用單一事務備份 InnoDB 適用於大數據量的備份',
quick: '逐行讀取數據而不是將整個表加載到內存中適用於大數據量和低內存機器的備份',
skipLockTables: '不鎖定所有表進行備份適用於高併發的資料庫',
missBackupAccount: '未能找到備份帳號',
syncDate: '同步時間 ',
clean: '快取清理',
+5
View File
@@ -1075,6 +1075,11 @@ const message = {
rulesHelper: '支持多个排除规则使用英文逗号 , 分隔例如*.log,*.sql',
lastRecordTime: '上次执行情况',
database: '备份数据库',
backupArgs: '备份参数',
backupArgsHelper: '未列出的备份参数可手动输入并选择: 输入 --no-data 后选择下拉列表中的第一个',
singleTransaction: '使用单一事务备份 InnoDB 适用于大数据量的备份',
quick: '逐行读取数据而不是将整个表加载到内存中适用于大数据量和低内存机器的备份',
skipLockTables: '不锁定所有表进行备份适用于高并发的数据库',
missBackupAccount: '未能找到备份账号',
syncDate: '同步时间 ',
clean: '缓存清理',
@@ -21,6 +21,11 @@ export const weekOptions = [
{ label: i18n.global.t('cronjob.saturday'), value: 6 },
{ label: i18n.global.t('cronjob.sunday'), value: 0 },
];
export const mysqlArgs = [
{ arg: '--single-transaction', description: i18n.global.t('cronjob.singleTransaction') },
{ arg: '--quick', description: i18n.global.t('cronjob.quick') },
{ arg: '--skip-lock-tables', description: i18n.global.t('cronjob.skipLockTables') },
];
function loadWeek(i: number) {
for (const week of weekOptions) {
if (week.value === i) {
@@ -335,6 +335,30 @@
</el-select>
</el-form-item>
</LayoutCol>
<LayoutCol
:span="20"
v-if="
form.type === 'database' &&
(form.dbType === 'mysql' || form.dbType === 'mysql-cluster')
"
>
<el-form-item :label="$t('cronjob.backupArgs')">
<el-select v-model="form.argItems" filterable allow-create multiple>
<el-option
v-for="item in mysqlArgs"
:key="item.arg"
:value="item.arg"
:label="item.arg"
>
{{ item.arg }}
<span class="ml-2">{{ item.description }}</span>
</el-option>
</el-select>
<span class="input-help">
{{ $t('cronjob.backupArgsHelper') }}
</span>
</el-form-item>
</LayoutCol>
<LayoutCol v-if="form.type === 'directory'">
<el-form-item :label="$t('commons.button.backup')">
<el-radio-group v-model="form.isDir" class="w-full">
@@ -774,7 +798,6 @@ import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import InputTag from '@/components/input-tag/index.vue';
import LayoutCol from '@/components/layout-col/form.vue';
import CleanLogConfig from '@/views/cronjob/cronjob/config/clean-log.vue';
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import { listBackupOptions } from '@/api/modules/backup';
@@ -797,6 +820,7 @@ import {
transSpecToObj,
weekOptions,
cronjobTypes,
mysqlArgs,
} from '../helper';
import { loadUsers } from '@/api/modules/toolbox';
import { loadContainerUsers } from '@/api/modules/container';
@@ -883,6 +907,8 @@ const form = reactive<Cronjob.CronjobInfo>({
alertMethodItems: [],
scopes: [],
args: '',
argItems: [],
});
const search = async () => {
@@ -982,6 +1008,7 @@ const search = async () => {
form.alertMethodItems = [];
}
form.scopes = res.data.scopes;
form.argItems = res.data.args ? res.data.args.split(',') : [];
})
.catch(() => {
loading.value = false;
@@ -1481,6 +1508,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
form.url = form.urlItems.join(',');
form.sourceAccountIDs = form.sourceAccountItems.join(',');
form.spec = specs.join('&&');
form.args = form.argItems.join(',');
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;