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

但清理失败也不应覆盖原始转换错误。可以记录为第二错误并由后续清理器补偿。

练习题

  1. 为任务设计上传、中间、最终文件 TTL。
  2. 写一个 Node.js Dry Run 清理器,只打印将删除目录。
  3. 解释为什么不能只根据 ctime 理解“创建超过一天”。
  4. 比较 cron Bash 与 Node Worker 的适用边界。