Support absolute paths like /dev/null for log redirection

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-09 09:57:15 +00:00
parent 71073b8670
commit 51bc0dd8b1
5 changed files with 63 additions and 23 deletions

View File

@ -477,19 +477,33 @@ export default class CronService {
); );
let { id, command, log_path, log_name } = cron; let { id, command, log_path, log_name } = cron;
// Sanitize log_name to prevent path traversal
// Check if log_name is an absolute path (e.g., /dev/null)
const isAbsolutePath = log_name && log_name.startsWith('/');
let uniqPath: string;
let absolutePath: string;
let logPath: string;
if (isAbsolutePath) {
// Use absolute path directly for special files like /dev/null
uniqPath = log_name!;
absolutePath = log_name!;
logPath = log_name!;
} else {
// Sanitize log_name to prevent path traversal for relative paths
const sanitizedLogName = log_name const sanitizedLogName = log_name
? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '') ? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '')
: ''; : '';
const uniqPath = uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`));
sanitizedLogName || (await getUniqPath(command, `${id}`));
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logDirPath = path.resolve(config.logPath, `${uniqPath}`); const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
if (log_path?.split('/')?.every((x) => x !== uniqPath)) { if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
await fs.mkdir(logDirPath, { recursive: true }); await fs.mkdir(logDirPath, { recursive: true });
} }
const logPath = `${uniqPath}/${logTime}.log`; logPath = `${uniqPath}/${logTime}.log`;
const absolutePath = path.resolve(config.logPath, `${logPath}`); absolutePath = path.resolve(config.logPath, `${logPath}`);
}
const cp = spawn( const cp = spawn(
`real_log_path=${logPath} no_delay=true ${this.makeCommand( `real_log_path=${logPath} no_delay=true ${this.makeCommand(
cron, cron,

View File

@ -41,8 +41,21 @@ export const commonCronSchema = {
.optional() .optional()
.allow('') .allow('')
.allow(null) .allow(null)
.pattern(/^[a-zA-Z0-9_-]+$/) .custom((value, helpers) => {
.max(100) if (!value) return value;
// Allow absolute paths like /dev/null
if (value.startsWith('/')) {
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({ .messages({
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符', 'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
'string.max': '日志名称不能超过100个字符', 'string.max': '日志名称不能超过100个字符',

View File

@ -524,7 +524,9 @@
"清除成功": "Clean successful", "清除成功": "Clean successful",
"日志名称": "Log Name", "日志名称": "Log Name",
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate", "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null",
"请输入自定义日志文件夹名称": "Please enter a custom log folder name", "请输入自定义日志文件夹名称": "Please enter a custom log folder name",
"请输入自定义日志文件夹名称或绝对路径": "Please enter a custom log folder name or absolute path",
"日志名称只能包含字母、数字、下划线和连字符": "Log name can only contain letters, numbers, underscores and hyphens", "日志名称只能包含字母、数字、下划线和连字符": "Log name can only contain letters, numbers, underscores and hyphens",
"日志名称不能超过100个字符": "Log name cannot exceed 100 characters" "日志名称不能超过100个字符": "Log name cannot exceed 100 characters"
} }

View File

@ -524,7 +524,9 @@
"清除成功": "清除成功", "清除成功": "清除成功",
"日志名称": "日志名称", "日志名称": "日志名称",
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成", "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null",
"请输入自定义日志文件夹名称": "请输入自定义日志文件夹名称", "请输入自定义日志文件夹名称": "请输入自定义日志文件夹名称",
"请输入自定义日志文件夹名称或绝对路径": "请输入自定义日志文件夹名称或绝对路径",
"日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符", "日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符",
"日志名称不能超过100个字符": "日志名称不能超过100个字符" "日志名称不能超过100个字符": "日志名称不能超过100个字符"
} }

View File

@ -184,22 +184,31 @@ const CronModal = ({
name="log_name" name="log_name"
label={intl.get('日志名称')} label={intl.get('日志名称')}
tooltip={intl.get( tooltip={intl.get(
'自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成', '自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null',
)} )}
rules={[ rules={[
{ {
pattern: /^[a-zA-Z0-9_-]*$/, validator: (_, value) => {
message: intl.get('日志名称只能包含字母、数字、下划线和连字符'), if (!value) return Promise.resolve();
// Allow absolute paths
if (value.startsWith('/')) return Promise.resolve();
// For relative names, enforce strict pattern
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
return Promise.reject(
intl.get('日志名称只能包含字母、数字、下划线和连字符'),
);
}
if (value.length > 100) {
return Promise.reject(intl.get('日志名称不能超过100个字符'));
}
return Promise.resolve();
}, },
{
max: 100,
message: intl.get('日志名称不能超过100个字符'),
}, },
]} ]}
> >
<Input <Input
placeholder={intl.get('请输入自定义日志文件夹名称')} placeholder={intl.get('请输入自定义日志文件夹名称或绝对路径')}
maxLength={100} maxLength={200}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item