fix: 计划任务部分 bug 修改

This commit is contained in:
ssongliu 2023-02-24 12:26:23 +08:00 committed by ssongliu
parent 7fdffa6848
commit 4849fde8c9
10 changed files with 102 additions and 62 deletions

View file

@ -93,4 +93,5 @@ type Record struct {
Message string `json:"message"` Message string `json:"message"`
TargetPath string `json:"targetPath"` TargetPath string `json:"targetPath"`
Interval int `json:"interval"` Interval int `json:"interval"`
File string `json:"file"`
} }

View file

@ -126,6 +126,7 @@ func (u *CronjobRepo) EndRecords(record model.JobRecords, status, message, recor
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["records"] = records errMap["records"] = records
errMap["status"] = status errMap["status"] = status
errMap["file"] = record.File
errMap["message"] = message errMap["message"] = message
errMap["interval"] = time.Since(record.StartTime).Milliseconds() errMap["interval"] = time.Since(record.StartTime).Milliseconds()
if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil { if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil {

View file

@ -107,7 +107,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
} }
fileName = fileName + ".tar.gz" fileName = fileName + ".tar.gz"
default: default:
fileName = fmt.Sprintf("%s.tar.gz", startTime.Format("20060102150405")) fileName = fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format("20060102150405"))
backupDir = fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name) backupDir = fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name)
global.LOG.Infof("handle tar %s to %s", backupDir, fileName) global.LOG.Infof("handle tar %s to %s", backupDir, fileName)
if err := handleTar(cronjob.SourceDir, baseDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil { if err := handleTar(cronjob.SourceDir, baseDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil {
@ -214,10 +214,10 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
if len(exclude) == 0 { if len(exclude) == 0 {
continue continue
} }
excludeRules += (" --exclude" + exclude) excludeRules += (" --exclude " + exclude)
} }
path := "" path := ""
if len(strings.Split(sourceDir, "/")) > 3 { if strings.Contains(sourceDir, "/") {
itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "")
aheadDir := strings.ReplaceAll(sourceDir, itemDir, "") aheadDir := strings.ReplaceAll(sourceDir, itemDir, "")
path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) path += fmt.Sprintf("-C %s %s", aheadDir, itemDir)
@ -225,6 +225,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
path = sourceDir path = sourceDir
} }
global.LOG.Debugf("tar zcvf %s %s %s \n", targetDir+"/"+name, excludeRules, path)
stdout, err := cmd.Execf("tar zcvf %s %s %s", targetDir+"/"+name, excludeRules, path) stdout, err := cmd.Execf("tar zcvf %s %s %s", targetDir+"/"+name, excludeRules, path)
if err != nil { if err != nil {
return errors.New(string(stdout)) return errors.New(string(stdout))

View file

@ -80,6 +80,19 @@ const checkImageName = (rule: any, value: any, callback: any) => {
} }
}; };
const checkVolumeName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.volumeName')));
} else {
const reg = /^[a-zA-Z0-9]{1}[a-z:A-Z0-9_.-]{0,30}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.volumeName')));
} else {
callback();
}
}
};
const checkLinuxName = (rule: any, value: any, callback: any) => { const checkLinuxName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) { if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.linuxName', ['/\\:*?"<>|']))); callback(new Error(i18n.global.t('commons.rule.linuxName', ['/\\:*?"<>|'])));
@ -128,6 +141,7 @@ interface CommonRule {
simpleName: FormItemRule; simpleName: FormItemRule;
dbName: FormItemRule; dbName: FormItemRule;
imageName: FormItemRule; imageName: FormItemRule;
volumeName: FormItemRule;
linuxName: FormItemRule; linuxName: FormItemRule;
password: FormItemRule; password: FormItemRule;
email: FormItemRule; email: FormItemRule;
@ -172,6 +186,11 @@ export const Rules: CommonRule = {
validator: checkImageName, validator: checkImageName,
trigger: 'blur', trigger: 'blur',
}, },
volumeName: {
required: true,
validator: checkVolumeName,
trigger: 'blur',
},
name: { name: {
required: true, required: true,
validator: checkName, validator: checkName,

View file

@ -123,6 +123,7 @@ export default {
simpleName: 'Support English, numbers and _ length 1-30', simpleName: 'Support English, numbers and _ length 1-30',
dbName: 'Support English, Chinese, numbers, .-, and _ length 1-16', dbName: 'Support English, Chinese, numbers, .-, and _ length 1-16',
imageName: 'Support English, Chinese, numbers, :.-_, length 1-30', imageName: 'Support English, Chinese, numbers, :.-_, length 1-30',
volumeName: 'Support English, numbers, .-_, length 1-30',
complexityPassword: complexityPassword:
'Please enter a password with more than 8 characters and must contain letters, digits, and special symbols', 'Please enter a password with more than 8 characters and must contain letters, digits, and special symbols',
commonPassword: 'Please enter a password with more than 6 characters', commonPassword: 'Please enter a password with more than 6 characters',
@ -526,18 +527,18 @@ export default {
enableMsg: 'The cronjob has been stopped. Enable now?', enableMsg: 'The cronjob has been stopped. Enable now?',
taskType: 'Task type', taskType: 'Task type',
record: 'Records', record: 'Records',
shell: 'shell', shell: 'Shell script',
website: 'website', website: 'Backup website',
rulesHelper: 'Compression exclusion rules (with; Is a delimiter), for example: \n*.log; *.sql', rulesHelper: 'Compression exclusion rules (with; Is a delimiter), for example: \n*.log; *.sql',
lastRecrodTime: 'Last execution time', lastRecrodTime: 'Last execution time',
all: 'All', all: 'All',
failedRecord: 'Failed records', failedRecord: 'Failed records',
successRecord: 'Successful records', successRecord: 'Successful records',
database: 'database', database: 'Backup database',
missBackupAccount: 'The backup account could not be found', missBackupAccount: 'The backup account could not be found',
syncDate: 'Synchronization time ', syncDate: 'Synchronization time ',
releaseMemory: 'Free memory', releaseMemory: 'Free memory',
curl: 'Crul', curl: 'Access URL',
taskName: 'Task name', taskName: 'Task name',
cronSpec: 'Lifecycle', cronSpec: 'Lifecycle',
directory: 'Backup directory', directory: 'Backup directory',
@ -548,9 +549,9 @@ export default {
target: 'Target', target: 'Target',
retainCopies: 'Retain copies', retainCopies: 'Retain copies',
cronSpecRule: 'Please enter a correct lifecycle', cronSpecRule: 'Please enter a correct lifecycle',
perMonth: 'Per monthly', perMonth: 'Every monthly',
perWeek: 'Per week', perWeek: 'Every week',
perHour: 'Per hour', perHour: 'Every hour',
perNDay: 'Every N days', perNDay: 'Every N days',
perDay: 'Every days', perDay: 'Every days',
perNHour: 'Every N hours', perNHour: 'Every N hours',
@ -572,6 +573,8 @@ export default {
errRecord: 'Incorrect logging', errRecord: 'Incorrect logging',
errHandle: 'Task execution failure', errHandle: 'Task execution failure',
noRecord: 'The execution did not generate any logs', noRecord: 'The execution did not generate any logs',
noLogs: 'No task output yet...',
errPath: 'Backup path [{0}] error, cannot download!',
}, },
monitor: { monitor: {
avgLoad: 'Average load', avgLoad: 'Average load',

View file

@ -128,6 +128,7 @@ export default {
simpleName: '支持英文数字_,长度1-30', simpleName: '支持英文数字_,长度1-30',
dbName: '支持英文中文数字.-_,长度1-16', dbName: '支持英文中文数字.-_,长度1-16',
imageName: '支持英文中文数字:.-_,长度1-30', imageName: '支持英文中文数字:.-_,长度1-30',
volumeName: '支持英文数字.-和_,长度1-30',
complexityPassword: '请输入 8 位以上必须含有字母数字特殊符号的密码', complexityPassword: '请输入 8 位以上必须含有字母数字特殊符号的密码',
commonPassword: '请输入 6 位以上长度密码', commonPassword: '请输入 6 位以上长度密码',
linuxName: '长度1-30名称不能含有{0}等符号', linuxName: '长度1-30名称不能含有{0}等符号',
@ -581,6 +582,8 @@ export default {
errRecord: '错误的日志记录', errRecord: '错误的日志记录',
errHandle: '任务执行失败', errHandle: '任务执行失败',
noRecord: '当前计划任务暂未产生记录', noRecord: '当前计划任务暂未产生记录',
noLogs: '暂无任务输出...',
errPath: '备份路径 [{0}] 错误无法下载',
}, },
monitor: { monitor: {
avgLoad: '平均负载', avgLoad: '平均负载',

View file

@ -173,7 +173,7 @@ const search = async () => {
loading.value = false; loading.value = false;
data.value = res.data.items || []; data.value = res.data.items || [];
for (const item of data.value) { for (const item of data.value) {
if (item.targetDir !== '-' || item.targetDir !== '') { if (item.targetDir !== '-' && item.targetDir !== '') {
item.targetDir = i18n.global.t('setting.' + item.targetDir); item.targetDir = i18n.global.t('setting.' + item.targetDir);
} }
} }

View file

@ -130,7 +130,7 @@
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="dialogData.rowData!.type === 'website' || dialogData.rowData!.type === 'directory'" v-if="dialogData.rowData!.type === 'directory'"
:label="$t('cronjob.exclusionRules')" :label="$t('cronjob.exclusionRules')"
prop="exclusionRules" prop="exclusionRules"
> >
@ -282,7 +282,7 @@ const weekOptions = [
{ label: i18n.global.t('cronjob.sunday'), value: 7 }, { label: i18n.global.t('cronjob.sunday'), value: 7 },
]; ];
const rules = reactive({ const rules = reactive({
name: [Rules.requiredInput, Rules.name], name: [Rules.requiredInput],
type: [Rules.requiredSelect], type: [Rules.requiredSelect],
specType: [Rules.requiredSelect], specType: [Rules.requiredSelect],
spec: [ spec: [

View file

@ -183,7 +183,7 @@
style="margin-left: 10px" style="margin-left: 10px"
link link
icon="Download" icon="Download"
@click="onDownload(currentRecord!.id, dialogData.rowData!.targetDirID)" @click="onDownload(currentRecord, dialogData.rowData!.targetDirID)"
> >
{{ $t('file.download') }} {{ $t('file.download') }}
</el-button> </el-button>
@ -253,7 +253,7 @@
<codemirror <codemirror
ref="mymirror" ref="mymirror"
:autofocus="true" :autofocus="true"
placeholder="None data" :placeholder="$t('cronjob.noLogs')"
:indent-with-tab="true" :indent-with-tab="true"
:tabSize="4" :tabSize="4"
style="height: 130px; width: 100%; margin-top: 5px" style="height: 130px; width: 100%; margin-top: 5px"
@ -288,7 +288,7 @@ import { reactive, ref } from 'vue';
import { Cronjob } from '@/api/interface/cronjob'; import { Cronjob } from '@/api/interface/cronjob';
import { loadZero } from '@/utils/util'; import { loadZero } from '@/utils/util';
import { searchRecords, download, handleOnce, updateStatus } from '@/api/modules/cronjob'; import { searchRecords, download, handleOnce, updateStatus } from '@/api/modules/cronjob';
import { dateFormat, dateFormatForName } from '@/utils/util'; import { dateFormat } from '@/utils/util';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { LoadFile } from '@/api/modules/files'; import { LoadFile } from '@/api/modules/files';
@ -296,7 +296,7 @@ import LayoutContent from '@/layout/layout-content.vue';
import { Codemirror } from 'vue-codemirror'; import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
const loading = ref(); const loading = ref();
const hasRecords = ref(); const hasRecords = ref();
@ -334,7 +334,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
hasRecords.value = true; hasRecords.value = true;
currentRecord.value = records.value[0]; currentRecord.value = records.value[0];
currentRecordIndex.value = 0; currentRecordIndex.value = 0;
loadRecord(currentRecord.value.records); loadRecord(currentRecord.value);
searchInfo.recordTotal = res.data.total; searchInfo.recordTotal = res.data.total;
recordShow.value = true; recordShow.value = true;
}; };
@ -446,13 +446,26 @@ const search = async () => {
endTime: searchInfo.endTime, endTime: searchInfo.endTime,
status: searchInfo.status, status: searchInfo.status,
}; };
records.value = [];
const res = await searchRecords(params); const res = await searchRecords(params);
records.value = res.data.items || []; if (!res.data.items) {
hasRecords.value = false;
return;
}
records.value = res.data.items;
hasRecords.value = true;
currentRecord.value = records.value[0];
currentRecordIndex.value = 0;
loadRecord(currentRecord.value);
searchInfo.recordTotal = res.data.total; searchInfo.recordTotal = res.data.total;
}; };
const onDownload = async (recordID: number, backupID: number) => { const onDownload = async (record: any, backupID: number) => {
if (!record.file || record.file.indexOf('/') === -1) {
MsgError(i18n.global.t('cronjob.errPath', [record.file]));
return;
}
let params = { let params = {
recordID: recordID, recordID: record.id,
backupAccountID: backupID, backupAccountID: backupID,
}; };
const res = await download(params); const res = await download(params);
@ -460,15 +473,9 @@ const onDownload = async (recordID: number, backupID: number) => {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
a.href = downloadUrl; a.href = downloadUrl;
if (dialogData.value.rowData!.type === 'database') { if (record.file && record.file.indexOf('/') !== -1) {
a.download = let pathItem = record.file.split('/');
dialogData.value.rowData!.dbName + '_' + dateFormatForName(currentRecord.value?.startTime) + '.sql.gz'; a.download = pathItem[pathItem.length - 1];
} else if (dialogData.value.rowData!.type === 'website') {
a.download =
dialogData.value.rowData!.website + '_' + dateFormatForName(currentRecord.value?.startTime) + '.tar.gz';
} else {
let name = dialogData.value.rowData!.sourceDir.replaceAll('/', '_');
a.download = name + '_' + dateFormatForName(currentRecord.value?.startTime) + '.tar.gz';
} }
const event = new MouseEvent('click'); const event = new MouseEvent('click');
a.dispatchEvent(event); a.dispatchEvent(event);
@ -484,11 +491,15 @@ const nextPage = async () => {
const forDetail = async (row: Cronjob.Record, index: number) => { const forDetail = async (row: Cronjob.Record, index: number) => {
currentRecord.value = row; currentRecord.value = row;
currentRecordIndex.value = index; currentRecordIndex.value = index;
loadRecord(row.records); loadRecord(row);
}; };
const loadRecord = async (path: string) => { const loadRecord = async (row: Cronjob.Record) => {
if (path) { if (row.status === 'Failed') {
const res = await LoadFile({ path: path }); currentRecordDetail.value = row.records;
return;
}
if (row.records) {
const res = await LoadFile({ path: row.records });
currentRecordDetail.value = res.data; currentRecordDetail.value = res.data;
} }
}; };
@ -521,7 +532,6 @@ defineExpose({
height: 310px; height: 310px;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none;
} }
.infinite-list .infinite-list-item { .infinite-list .infinite-list-item {
display: flex; display: flex;

View file

@ -1,52 +1,54 @@
<template> <template>
<el-row> <div>
<el-col :span="22" :offset="1"> <el-form label-position="top">
<el-descriptions :column="4" direction="vertical"> <el-row type="flex" style="margin-left: 50px" justify="center">
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.connections') }}</span> <span class="status-label">{{ $t('database.connections') }}</span>
</template> </template>
<span class="status-count">{{ data.active }}</span> <span class="status-count">{{ data.active }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.accepts') }}</span> <span class="status-label">{{ $t('database.accepts') }}</span>
</template> </template>
<span class="status-count">{{ data.accepts }}</span> <span class="status-count">{{ data.accepts }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.handled') }}</span> <span class="status-label">{{ $t('database.handled') }}</span>
</template> </template>
<span class="status-count">{{ data.handled }}</span> <span class="status-count">{{ data.handled }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.requests') }}</span> <span class="status-label">{{ $t('database.requests') }}</span>
</template> </template>
<span class="status-count">{{ data.requests }}</span> <span class="status-count">{{ data.requests }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item>
<el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.reading') }}</span> <span class="status-label">{{ $t('database.reading') }}</span>
</template> </template>
<span class="status-count">{{ data.reading }}</span> <span class="status-count">{{ data.reading }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.writing') }}</span> <span class="status-label">{{ $t('database.writing') }}</span>
</template> </template>
<span class="status-count">{{ data.writing }}</span> <span class="status-count">{{ data.writing }}</span>
</el-descriptions-item> </el-form-item>
<el-descriptions-item> <el-form-item style="width: 25%">
<template #label> <template #label>
<span class="status-label">{{ $t('nginx.waiting') }}</span> <span class="status-label">{{ $t('database.waiting') }}</span>
</template> </template>
<span class="status-count">{{ data.waiting }}</span> <span class="status-count">{{ data.waiting }}</span>
</el-descriptions-item> </el-form-item>
</el-descriptions> <el-form-item style="width: 25%" />
</el-col> </el-row>
</el-row> </el-form>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>