18 临时目录、上传文件与清理
1. 每个任务独立目录
TEMP_DIR/jobs/<taskId>/
├─upload/
├─extract/
├─rendered/
└─output/
不要让所有请求共享一个 temp/ 根目录并直接使用原始文件名,否则同名请求会覆盖,清理也难以确认归属。
2. 创建安全临时目录
import { mkdtemp } from "node:fs/promises";
import path from "node:path";
const taskDirectory = await mkdtemp(
path.join(tempJobsRoot, "markdown-export-"),
);
mkdtemp() 会在前缀后添加随机字符。调用前先确保受控根目录存在。
3. try/finally
let taskDirectory: string | undefined;
try {
taskDirectory = await createTaskDirectory();
await processUpload(taskDirectory);
} finally {
if (taskDirectory !== undefined) {
await rm(taskDirectory, {
recursive: true,
force: true,
});
}
}
如果结果要通过 Stream 下载,不能在调用 res.download() 后立刻清理;应等待响应完成/关闭,并处理客户端中断。更简单的生产架构是先把任务结果保存为有生命周期的文件,由独立清理器删除。
4. 三类文件生命周期
| 类型 | 示例 | 清理时机 |
|---|---|---|
| 上传原件 | Multer ZIP | 校验/任务接管后按策略删除 |
| 中间产物 | 解压 Markdown、HTML | 任务完成或失败后 |
| 最终结果 | HTML ZIP | 下载后、TTL 后或用户删除后 |
最终文件是否“一次下载后删除”要由业务决定,不能在网络传输尚未结束时删除。
5. 时间字段
mtime 内容修改时间
ctime 元数据变更时间,不是通用创建时间
birthtime 文件系统提供的创建时间,跨平台可靠性有限
按“存放超过一天”清理时通常选择目录/文件 mtime,但 Worker 写入会刷新时间。更可靠的是任务状态记录 expiresAt;没有数据库记录时,需要明确以哪个文件或目录的 mtime 为准。
6. Node.js 与 Bash
Ubuntu 单机纯运维目录可使用:
find /srv/nloop/tmp/jobs \
-mindepth 1 -maxdepth 1 \
-type d -mmin +1440 \
-print
确认 Dry Run 后才增加删除:
find /srv/nloop/tmp/jobs \
-mindepth 1 -maxdepth 1 \
-type d -mmin +1440 \
-exec rm -rf -- {} +
Bash 简洁,适合 systemd timer/cron;Node.js 更容易结合任务状态、Pino、Windows 和“正在使用”判断。无论选择哪种,都先固定绝对根目录、限制深度并 Dry Run。
7. 什么时候清理
推荐组合:
请求/任务结束:立即尽力清理中间文件
服务启动:扫描上次崩溃遗留目录
低峰期定时任务:清理超过 TTL 的最终结果和遗留任务
磁盘告警:触发人工或受控加速清理
定时清理仍要避免多个 PM2 进程重复执行。可以单独运行一个清理进程、使用分布式锁,或交给 systemd timer。
8. 清理失败
清理属于重要操作,不应完全静默:
taskId
target relative path
error.code
attempt
next retry time
但清理失败也不应覆盖原始转换错误。可以记录为第二错误并由后续清理器补偿。
练习题
- 为任务设计上传、中间、最终文件 TTL。
- 写一个 Node.js Dry Run 清理器,只打印将删除目录。
- 解释为什么不能只根据
ctime理解“创建超过一天”。 - 比较 cron Bash 与 Node Worker 的适用边界。