23 最终实战:安全 Markdown ZIP 转换服务

本章不给出完整实现,而是提供可执行需求、函数接口和验收清单。完成后应能解释每个完成事件和清理动作,而不是只看到一次正常请求成功。

1. 输入格式

示例 ZIP:

project/
├─README.md
├─guide/
│  ├─start.md
│  └─images/logo.png
└─reference/
   └─api.md

输出:

project/
├─README.html
├─guide/
│  ├─start.html
│  └─images/logo.png
└─reference/
   └─api.html

是否保留最外层 project/ 要成为显式配置,不能偶然取决于 Archiver 参数。

2. HTTP 契约

POST /api/markdown-exports
Authorization: Bearer <token>
Content-Type: multipart/form-data

短任务可直接返回 ZIP;推荐进阶设计:

{
  "code": "OK",
  "message": "任务已创建",
  "data": {
    "taskId": "...",
    "status": "queued"
  }
}

查询:

GET /api/markdown-exports/:taskId
GET /api/markdown-exports/:taskId/download

3. 类型骨架

export interface MarkdownExportLimits {
  maxArchiveBytes: number;
  maxEntryCount: number;
  maxEntryBytes: number;
  maxExtractedBytes: number;
  maxMarkdownBytes: number;
  maxPathLength: number;
}

export interface MarkdownExportContext {
  taskId: string;
  uploadedZipPath: string;
  extractDirectory: string;
  renderedDirectory: string;
  temporaryZipPath: string;
  finalZipPath: string;
  limits: MarkdownExportLimits;
}

需要自行实现:

createTaskContext()
validateZipEntryPath()
extractZipSafely()
findMarkdownFiles()
renderMarkdownFile()
copyStaticAsset()
archiveDirectory()
publishResult()
cleanupTaskDirectory()

4. 功能要求

5. 安全要求

6. Stream 要求

7. 并发要求

8. 错误码建议

UPLOAD_REQUIRED
ARCHIVE_TOO_LARGE
INVALID_ZIP
INVALID_ZIP_ENTRY_PATH
ZIP_ENTRY_LIMIT_EXCEEDED
EXTRACTED_SIZE_LIMIT_EXCEEDED
MARKDOWN_TOO_LARGE
MARKDOWN_RENDER_FAILED
ARCHIVE_CREATE_FAILED
FILE_STORAGE_ERROR
TASK_NOT_FOUND
EXPORT_NOT_READY

项目约定业务错误 HTTP 200 时,Pino 必须记录 businessCode;响应头已经发出后的下载错误不保证还能返回 JSON。

9. 必测输入

正常多层目录 ZIP
空 ZIP
损坏 ZIP
只有目录没有文件
两个目录分别有 index.md
重复 Entry
../server.ts
/etc/passwd
C:\Windows\...
混合 / 和 \
大小写冲突 README.md/readme.md
单个超大 Markdown
大量小文件
高压缩比文件
Markdown 中 script 和 javascript: 链接
目标磁盘不可写
目标磁盘空间不足
客户端下载中断
任务处理中服务退出

10. 手工验收

curl -i \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@sample.zip" \
  http://127.0.0.1:8080/api/markdown-exports

Windows PowerShell 中变量和路径语法不同,应按当前 Shell 调整,不要机械复制 Bash 续行符。

下载后检查:

ZIP 能正常打开
HTML 数量正确
目录层级正确
图片链接有效
代码高亮 span 含 class
危险 HTML 已清理
无任务临时文件泄漏
无持续句柄/内存增长

11. 自动化测试方向

12. 最终复盘题

  1. endfinishclose 在这条链路分别属于谁?
  2. yauzl、WriteStream、Archiver、HTTP Response 任一报错时销毁哪些资源?
  3. 为什么 ZIP Header 大小不能作为唯一限制?
  4. 为什么目录层级消失不一定是 archive.directory() 的问题?
  5. 怎样证明连续处理 100 次后没有明显资源泄漏?