diff --git a/backend/app/dto/cronjob.go b/backend/app/dto/cronjob.go index 07f1dbd68..5f3969bc3 100644 --- a/backend/app/dto/cronjob.go +++ b/backend/app/dto/cronjob.go @@ -93,4 +93,5 @@ type Record struct { Message string `json:"message"` TargetPath string `json:"targetPath"` Interval int `json:"interval"` + File string `json:"file"` } diff --git a/backend/app/repo/cronjob.go b/backend/app/repo/cronjob.go index 3c6852014..1e7068c51 100644 --- a/backend/app/repo/cronjob.go +++ b/backend/app/repo/cronjob.go @@ -126,6 +126,7 @@ func (u *CronjobRepo) EndRecords(record model.JobRecords, status, message, recor errMap := make(map[string]interface{}) errMap["records"] = records errMap["status"] = status + errMap["file"] = record.File errMap["message"] = message errMap["interval"] = time.Since(record.StartTime).Milliseconds() if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil { diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index 6cdb7b81b..38525984e 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -107,7 +107,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim } fileName = fileName + ".tar.gz" 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) global.LOG.Infof("handle tar %s to %s", backupDir, fileName) 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 { continue } - excludeRules += (" --exclude" + exclude) + excludeRules += (" --exclude " + exclude) } path := "" - if len(strings.Split(sourceDir, "/")) > 3 { + if strings.Contains(sourceDir, "/") { itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") aheadDir := strings.ReplaceAll(sourceDir, itemDir, "") path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) @@ -225,6 +225,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error { 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) if err != nil { return errors.New(string(stdout)) diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index f881a8d74..8ef4d24af 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -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) => { if (value === '' || typeof value === 'undefined' || value == null) { callback(new Error(i18n.global.t('commons.rule.linuxName', ['/\\:*?"<>|']))); @@ -128,6 +141,7 @@ interface CommonRule { simpleName: FormItemRule; dbName: FormItemRule; imageName: FormItemRule; + volumeName: FormItemRule; linuxName: FormItemRule; password: FormItemRule; email: FormItemRule; @@ -172,6 +186,11 @@ export const Rules: CommonRule = { validator: checkImageName, trigger: 'blur', }, + volumeName: { + required: true, + validator: checkVolumeName, + trigger: 'blur', + }, name: { required: true, validator: checkName, diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c44b7dd83..2432c4961 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -123,6 +123,7 @@ export default { simpleName: 'Support English, numbers and _ length 1-30', dbName: 'Support English, Chinese, numbers, .-, and _ length 1-16', imageName: 'Support English, Chinese, numbers, :.-_, length 1-30', + volumeName: 'Support English, numbers, .-_, length 1-30', complexityPassword: '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', @@ -526,18 +527,18 @@ export default { enableMsg: 'The cronjob has been stopped. Enable now?', taskType: 'Task type', record: 'Records', - shell: 'shell', - website: 'website', + shell: 'Shell script', + website: 'Backup website', rulesHelper: 'Compression exclusion rules (with; Is a delimiter), for example: \n*.log; *.sql', lastRecrodTime: 'Last execution time', all: 'All', failedRecord: 'Failed records', successRecord: 'Successful records', - database: 'database', + database: 'Backup database', missBackupAccount: 'The backup account could not be found', syncDate: 'Synchronization time ', releaseMemory: 'Free memory', - curl: 'Crul', + curl: 'Access URL', taskName: 'Task name', cronSpec: 'Lifecycle', directory: 'Backup directory', @@ -548,9 +549,9 @@ export default { target: 'Target', retainCopies: 'Retain copies', cronSpecRule: 'Please enter a correct lifecycle', - perMonth: 'Per monthly', - perWeek: 'Per week', - perHour: 'Per hour', + perMonth: 'Every monthly', + perWeek: 'Every week', + perHour: 'Every hour', perNDay: 'Every N days', perDay: 'Every days', perNHour: 'Every N hours', @@ -572,6 +573,8 @@ export default { errRecord: 'Incorrect logging', errHandle: 'Task execution failure', noRecord: 'The execution did not generate any logs', + noLogs: 'No task output yet...', + errPath: 'Backup path [{0}] error, cannot download!', }, monitor: { avgLoad: 'Average load', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index c849d9a1b..7399b0deb 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -128,6 +128,7 @@ export default { simpleName: '支持英文、数字、_,长度1-30', dbName: '支持英文、中文、数字、.-_,长度1-16', imageName: '支持英文、中文、数字、:.-_,长度1-30', + volumeName: '支持英文、数字、.-和_,长度1-30', complexityPassword: '请输入 8 位以上、必须含有字母、数字、特殊符号的密码', commonPassword: '请输入 6 位以上长度密码', linuxName: '长度1-30,名称不能含有{0}等符号', @@ -581,6 +582,8 @@ export default { errRecord: '错误的日志记录', errHandle: '任务执行失败', noRecord: '当前计划任务暂未产生记录', + noLogs: '暂无任务输出...', + errPath: '备份路径 [{0}] 错误,无法下载!', }, monitor: { avgLoad: '平均负载', diff --git a/frontend/src/views/cronjob/index.vue b/frontend/src/views/cronjob/index.vue index a547d603c..0b94bc46c 100644 --- a/frontend/src/views/cronjob/index.vue +++ b/frontend/src/views/cronjob/index.vue @@ -173,7 +173,7 @@ const search = async () => { loading.value = false; data.value = res.data.items || []; for (const item of data.value) { - if (item.targetDir !== '-' || item.targetDir !== '') { + if (item.targetDir !== '-' && item.targetDir !== '') { item.targetDir = i18n.global.t('setting.' + item.targetDir); } } diff --git a/frontend/src/views/cronjob/operate/index.vue b/frontend/src/views/cronjob/operate/index.vue index 238405901..035c6b435 100644 --- a/frontend/src/views/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/operate/index.vue @@ -130,7 +130,7 @@ @@ -282,7 +282,7 @@ const weekOptions = [ { label: i18n.global.t('cronjob.sunday'), value: 7 }, ]; const rules = reactive({ - name: [Rules.requiredInput, Rules.name], + name: [Rules.requiredInput], type: [Rules.requiredSelect], specType: [Rules.requiredSelect], spec: [ diff --git a/frontend/src/views/cronjob/record/index.vue b/frontend/src/views/cronjob/record/index.vue index df702d41b..135f3925a 100644 --- a/frontend/src/views/cronjob/record/index.vue +++ b/frontend/src/views/cronjob/record/index.vue @@ -183,7 +183,7 @@ style="margin-left: 10px" link icon="Download" - @click="onDownload(currentRecord!.id, dialogData.rowData!.targetDirID)" + @click="onDownload(currentRecord, dialogData.rowData!.targetDirID)" > {{ $t('file.download') }} @@ -253,7 +253,7 @@ => { hasRecords.value = true; currentRecord.value = records.value[0]; currentRecordIndex.value = 0; - loadRecord(currentRecord.value.records); + loadRecord(currentRecord.value); searchInfo.recordTotal = res.data.total; recordShow.value = true; }; @@ -446,13 +446,26 @@ const search = async () => { endTime: searchInfo.endTime, status: searchInfo.status, }; + records.value = []; 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; }; -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 = { - recordID: recordID, + recordID: record.id, backupAccountID: backupID, }; const res = await download(params); @@ -460,15 +473,9 @@ const onDownload = async (recordID: number, backupID: number) => { const a = document.createElement('a'); a.style.display = 'none'; a.href = downloadUrl; - if (dialogData.value.rowData!.type === 'database') { - a.download = - dialogData.value.rowData!.dbName + '_' + dateFormatForName(currentRecord.value?.startTime) + '.sql.gz'; - } 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'; + if (record.file && record.file.indexOf('/') !== -1) { + let pathItem = record.file.split('/'); + a.download = pathItem[pathItem.length - 1]; } const event = new MouseEvent('click'); a.dispatchEvent(event); @@ -484,11 +491,15 @@ const nextPage = async () => { const forDetail = async (row: Cronjob.Record, index: number) => { currentRecord.value = row; currentRecordIndex.value = index; - loadRecord(row.records); + loadRecord(row); }; -const loadRecord = async (path: string) => { - if (path) { - const res = await LoadFile({ path: path }); +const loadRecord = async (row: Cronjob.Record) => { + if (row.status === 'Failed') { + currentRecordDetail.value = row.records; + return; + } + if (row.records) { + const res = await LoadFile({ path: row.records }); currentRecordDetail.value = res.data; } }; @@ -521,7 +532,6 @@ defineExpose({ height: 310px; padding: 0; margin: 0; - list-style: none; } .infinite-list .infinite-list-item { display: flex; diff --git a/frontend/src/views/website/website/nginx/status/index.vue b/frontend/src/views/website/website/nginx/status/index.vue index 94a25d064..921652b49 100644 --- a/frontend/src/views/website/website/nginx/status/index.vue +++ b/frontend/src/views/website/website/nginx/status/index.vue @@ -1,52 +1,54 @@