Revel 框架需手动集成 Go 标准库 archive/zip 实现 ZIP 打包下载,关键包括:直接写入 ResponseWriter 避免临时文件、正确设置 FileHeader 的 FileInfo 和 ModTime、UTF-8 中文名直赋、分块读取防内存溢出、路径净化防穿越、并发隔离 zip.Writer、Content-Disposition 使用 url.PathEscape。

Revel 框架本身不内置 ZIP 打包能力,也没有类似 ZipArchive(PHP)或 ZipOutputStream(Java)那样的开箱即用组件。你要实现“多个文件打包成 ZIP 下载”,必须手动组合 Go 标准库的 archive/zip、io 和 HTTP 响应流——而且得小心处理边界:空文件、路径穿越、中文名编码、内存占用。
用 archive/zip 写入 ZIP 流,别写临时文件
Revel 的 Controller.Result 返回的是 http.ResponseWriter,你可以直接往它的 Write 方法里写 ZIP 数据流,避免落盘临时 ZIP 文件(否则要清理、可能权限失败、并发时冲突)。关键点是:zip.NewWriter 必须包装在 ResponseWriter 上,且每个 zip.FileHeader 要显式设置 FileInfo 和 ModTime,否则某些客户端解压后时间戳异常。
- 文件名含中文?必须用
zip.FileHeader.SetMode(0644)+header.Name直接赋 UTF-8 字符串(Go 1.16+ 原生支持,无需转 GBK) - 别用
os.Open后整个io.Copy——大文件会吃光内存;改用bufio.NewReader分块读(比如 32KB buffer) - 每个
zip.FileHeader创建后,必须调用writer.CreateHeader(header),再写内容;漏掉这步,文件就“消失”了
revel.Controller.Response.Out 不是标准 http.ResponseWriter,要转换
Revel 的 c.Response.Out 是 io.Writer,不是 http.ResponseWriter。你不能直接传给 zip.NewWriter——它需要能设置 Header() 的响应对象。正确做法是用 c.Request.Request 拿到原始 *http.Request,再通过 c.Response.Out 包装一个带 Header 控制能力的 wrapper,或者更简单:直接用 c.RenderBinary(但注意它会把整个 ZIP 加载进内存)。
- 推荐方案:用
c.Response.WriteHeader(http.StatusOK)+c.Response.Header().Set(...)显式设好Content-Type和Content-Disposition,然后io.Copy(c.Response.Out, zipWriter) - 错误示范:
c.RenderBinary(zipBytes, "files.zip")——如果文件总大小超 100MB,Go runtime 会 OOM -
Content-Disposition中的 filename= 部分,要用url.PathEscape("中文名.zip"),不是URLEncoder.encode(那是 Java)
文件路径来源不可信时,必须做路径净化
如果你从数据库或参数里拿到待打包的文件路径(比如 /uploads/invoice_2026.pdf),绝不能直接 os.Open(path)。攻击者可能传 ../../../etc/passwd 导致任意文件读取。
- 用
filepath.Clean(path)后,检查是否仍以白名单根目录开头,例如strings.HasPrefix(cleaned, "/var/www/uploads/") - 不要信任扩展名:PDF 文件可能被重命名为
.jpg,但实际是 ZIP;校验 MIME 类型要用net/http.DetectContentType或file命令(Go 中可用github.com/h2non/filetype) - 如果文件来自用户上传,确保它们存放在 Revel 的
app/tmp或独立存储路径,而非 Web 可访问目录下
并发打包多个请求时,zip.Writer 不是线程安全的
Revel 默认每个请求走独立 goroutine,但如果你在 controller 里复用了全局 *zip.Writer 实例,会 panic:write to closed pipe 或 data race。所有 ZIP 相关对象(zip.Writer、os.File、buffer)都必须在 handler 函数内 new。
- 别在
init()或 controller struct 里声明zipWriter *zip.Writer - 每个请求创建新
bytes.Buffer或直接写ResponseWriter,别跨请求复用 buffer - 如果打包逻辑抽成工具函数,签名必须是
func(w io.Writer, files []string) error,而不是接收预创建的 writer


















