import path from "node:path"; function trimSlashes(input: string): string { return input.trim().replace(/^\/+|\/+$/g, ""); } function hasParentSegment(input: string): boolean { return input .split("/") .map((part) => part.trim()) .some((part) => part === ".."); } /** 允许相对项目根或绝对路径;禁止含 `..` 的路径片段(防止配置逃逸)。 */ function ensureConfigurableDir(input: string, fallback: string, envName: string): string { const raw = input.trim(); if (!raw) { return fallback; } const normalized = path.normalize(raw); const segments = normalized.split(path.sep); if (segments.some((part) => part === "..")) { throw new Error(`${envName} must not contain ".." path segments`); } return normalized; } function ensureSafeSubdir(input: string, fallback: string, envName: string): string { const value = trimSlashes(input); if (!value) { return fallback; } if (hasParentSegment(value)) { throw new Error(`${envName} must not contain ".." segments`); } return value; } /** 静态资源 URL 前缀固定为 `/static`,不允许通过环境变量覆写。 */ export const STATIC_PUBLIC_PREFIX = "/static"; /** 静态资源根目录(相对项目根或绝对路径),默认 `static` */ export const STATIC_DIR = ensureConfigurableDir(process.env.STATIC_DIR ?? "static", "static", "STATIC_DIR"); /** 媒体上传子目录(相对 STATIC_DIR),默认 `upload` */ export const UPLOAD_SUBDIR = ensureSafeSubdir( process.env.UPLOAD_SUBDIR ?? "upload", "upload", "UPLOAD_SUBDIR", ); /** 媒体上传目录(与 STATIC_DIR 同为相对或绝对),默认 `static/upload` */ export const RELATIVE_ASSETS_DIR = path.join(STATIC_DIR, UPLOAD_SUBDIR); /** 与 `media` 返回及静态路径一致,无前导 host */ export const POST_MEDIA_PUBLIC_PREFIX = `${STATIC_PUBLIC_PREFIX}/${UPLOAD_SUBDIR}/`; /** 允许的上传文件 MIME 类型 */ export const ALLOWED_MIME_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'] as const; /** 单文件最大大小(字节) */ export const MAX_FILE_SIZE = 5 * 1024 * 1024; /** 单次上传最大文件数 */ export const MAX_FILE_COUNT = 10;