定时任务支持自定义日志文件或者 /dev/null (#2823)

* Initial plan

* Add log_name field to enable custom log folder naming

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Add database migration for log_name column

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Add security validation to prevent path traversal attacks

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Apply prettier formatting to modified files

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Support absolute paths like /dev/null for log redirection

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Restrict absolute paths to log directory except /dev/null

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
Copilot
2025-11-09 19:32:40 +08:00
committed by GitHub
parent 0e28e1b6c4
commit c369514741
7 changed files with 160 additions and 12 deletions
+41
View File
@@ -1,6 +1,8 @@
import { Joi } from 'celebrate';
import cron_parser from 'cron-parser';
import { ScheduleType } from '../interface/schedule';
import path from 'path';
import config from '../config';
const validateSchedule = (value: string, helpers: any) => {
if (
@@ -37,4 +39,43 @@ export const commonCronSchema = {
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
log_name: Joi.string()
.optional()
.allow('')
.allow(null)
.custom((value, helpers) => {
if (!value) return value;
// Check if it's an absolute path
if (value.startsWith('/')) {
// Allow /dev/null as special case
if (value === '/dev/null') {
return value;
}
// For other absolute paths, ensure they are within the safe log directory
const normalizedValue = path.normalize(value);
const normalizedLogPath = path.normalize(config.logPath);
if (!normalizedValue.startsWith(normalizedLogPath)) {
return helpers.error('string.unsafePath');
}
return value;
}
// For relative names, enforce strict pattern
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
return helpers.error('string.pattern.base');
}
if (value.length > 100) {
return helpers.error('string.max');
}
return value;
})
.messages({
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
'string.max': '日志名称不能超过100个字符',
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
}),
};