archive/zip 创建目录条目并保持文件权限的方法
来源:17golang原创
时间:2026-11-12 18:12:51 292浏览 收藏
Go 使用 archive/zip 创建带权限的目录和文件条目时,应先构造 FileHeader,再调用 CreateHeader。目录名称必须以 / 结尾且不写数据;权限用 SetMode 写入条目头,已有文件信息则可用 FileInfoHeader 生成初始元数据。归档结束必须检查 zip.Writer.Close 的错误,确保中央目录写入完成。
升级范围:从只写内容迁移到保留条目元数据
这里的升级指应用打包代码从“只保存路径与内容”迁移为“同时记录目录类型和权限”,不是某个 Go 新版本发布。本文保持源目录内部层级,显式记录空目录,并保存 ZIP 能表达的源模式位;不承诺把 ACL、扩展属性、属主和所有平台权限模型完整打包。
需要区分两件事:归档头里记录了什么,以及解压工具最终怎样创建文件。前者由生成器控制,后者还取决于解压实现、目标系统和策略。因此,“保持文件权限”应理解为正确记录相应元数据,并在使用环境里确认恢复行为。
变更表:把名称、类型和权限放回条目头

| 处理对象 | 仅写内容的写法 | 保留元数据的写法 |
|---|---|---|
| 普通文件 | Create(name) | FileInfoHeader 与 CreateHeader |
| 目录 | 只依赖子文件路径 | 显式名称以 / 结尾的条目 |
| 权限位 | 未主动承接源模式 | SetMode(info.Mode()) |
| 压缩方式 | Create 默认 Deflate | 普通文件显式 Deflate,目录 Store |
| 完成判定 | 复制结束就返回 | 继续检查 ZIP Close 与输出关闭 |
FileInfoHeader 得到的是部分填充的头,名称来源通常只有基本文件名,仍需改成根目录内的相对路径;需要压缩时也应设置 Method。归档名称使用正斜杠,不能直接照搬带盘符、开头斜杠或平台反斜杠的本地绝对路径。
旧代码风险:空目录和执行权限容易被遗漏
只写 assets/icon.txt 可以让很多解压器创建 assets,但这不等于归档中有独立的 assets/ 条目。如果目录完全为空,就没有子文件间接表达它,目录会从这种写法中消失。需要保留空目录时,必须显式写目录条目。
另一个常见问题是把每个文件都写成相同权限,导致脚本的执行位丢失。若目标是承接源权限,应从源 FileInfo 的 Mode 获取,而不是给所有文件硬编码 0644。若目标是发布统一权限的资源包,则可以主动设置,但那属于规范化策略,不能再称为原样保留。
目录必须带目录类型位。手动构造头时可使用 h.SetMode(fs.ModeDir | 0755),并把 Name 设为例如 assets/。单独的 0755 只是权限位,不能代替目录类型;反过来,只加尾斜杠也没有表达你希望保存的全部权限信息。
新写法:从源文件信息生成独立条目头

下面程序处理可信、稳定的普通目录,只接收普通文件和目录,遇到符号链接、设备等其他条目报错。示例先在内存中构建 ZIP,成功后才创建新的输出文件,适合小型资源包;大目录应改为流式临时文件,并在完整成功后发布结果。
package main
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
func buildZIP(root string) ([]byte, error) {
// 示例只打包可信且在打包期间保持稳定的目录。
info, err := os.Lstat(root)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, fmt.Errorf("源路径必须是普通目录")
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
walkErr := filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(root, p)
if err != nil {
return err
}
if rel == "." {
return nil // 本例不保存根目录自身,只保存内部条目。
}
info, err := d.Info()
if err != nil {
return err
}
if !info.IsDir() && !info.Mode().IsRegular() {
return fmt.Errorf("不支持的源条目: %s", rel)
}
name := filepath.ToSlash(rel)
// 避免 Unix 文件名中的反斜杠或冒号产生跨平台歧义。
if strings.ContainsAny(name, "\\:") {
return fmt.Errorf("不支持的归档名称: %s", name)
}
h, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
h.Name = name
h.SetMode(info.Mode()) // 保留可表达的类型位和权限位。
if info.IsDir() {
h.Name += "/"
h.Method = zip.Store
_, err = zw.CreateHeader(h)
return err // 目录没有数据,不向返回写入器写内容。
}
h.Method = zip.Deflate
entry, err := zw.CreateHeader(h)
if err != nil {
return err
}
f, err := os.Open(p)
if err != nil {
return err
}
_, copyErr := io.Copy(entry, f)
closeErr := f.Close() // 每个文件当场关闭,避免遍历中积累句柄。
return errors.Join(copyErr, closeErr)
})
// Close 写入中央目录;不能只凭遍历成功就返回归档。
closeErr := zw.Close()
if err := errors.Join(walkErr, closeErr); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func main() {
if len(os.Args) != 3 {
fmt.Fprintln(os.Stderr, "用法: go run . 源目录 新归档路径")
os.Exit(2)
}
data, err := buildZIP(os.Args[1])
if err == nil {
var f *os.File
// 不覆盖已有归档;输出是独立文件,权限不代表条目权限。
f, err = os.OpenFile(os.Args[2], os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err == nil {
_, writeErr := io.Copy(f, bytes.NewReader(data))
err = errors.Join(writeErr, f.Close())
}
}
if err != nil {
fmt.Fprintln(os.Stderr, "打包失败:", err)
os.Exit(1)
}
fmt.Println("归档已写入")
}
示例使用 errors.Join 合并复制与关闭错误,因此需要 Go 1.20 或更高版本;这不是 FileInfoHeader 或 SetMode 的新增版本限制。代码不保存源根目录本身,所以空源根目录得到空 ZIP;根目录内部的空子目录会生成目录条目。
CreateHeader 接管传入头的所有权,并可能修改它。每轮创建独立头,在调用后不继续改字段,不把同一个头反复修改后交给多个条目。普通文件的数据必须在创建下一个条目前完成写入,也不要并发向同一个 ZIP 写入器交错写不同文件。
ZIP Close 写中央目录,但不关闭底层写入器。内存缓冲区无需关闭;如果改成文件或网络流,底层资源由其拥有者收尾。示例最终输出文件使用 0600,只限定归档文件本身的访问权限,与包内每个文件的模式没有关系。
回归检查:区分归档记录与解压后状态
迁移后的应用可以按以下预期判读结果:源目录中的空子目录对应一个以 / 结尾的空条目;普通文件内容与名称对应;ZIP 读取方的 FileHeader.Mode 能表达预期的目录类型或文件权限。这是建议读者在自己环境中检查的项目,本文没有运行归档测试或声称取得实测输出。
继续观察解压后的状态时,要把问题分层。如果包头模式就不对,应看源文件 Mode 和头构造;包头正确而落盘权限不同,则看解压工具与目标文件系统是否支持、是否应用这些元数据。不要仅凭某个图形解压软件显示的图标判断 Unix 执行位是否保存。
示例不是并发变化目录的快照器。遍历与读取之间,源文件仍可能被其他程序替换或改写;需要可重复发布时,应先固定输入目录。输出磁盘写入失败也可能留下部分文件,代码会返回失败;生产任务可使用临时文件加成功后提交的策略,不能把存在一个 ZIP 文件等同于任务成功。
迁移清单与相关问题
迁移时先确定根目录是否入包,再统一相对名称和分隔符;目录独立建条目,文件承接源模式;明确特殊条目的处理规则;最后把复制错误、ZIP Close 和底层输出错误都纳入返回值。若已有发布流程依赖固定权限,应把这一策略写清楚,避免与源模式保留混用。
SetMode 是否等于解压时自动 chmod?不是。它修改归档头的权限与类型字段,不直接操作未来的解压目标文件。恢复动作由解压方实现。
目录需要压缩并写一个空字符串吗?不需要。目录名称以 / 结尾,不写数据即可;示例显式选择 Store,使意图清楚。
能否完整保留 Windows ACL 或 Unix 属主?本文方案不提供这类保证。ZIP 中的模式位与平台完整权限体系不是同一套信息,若发布要求依赖这些属性,应另行定义元数据格式与恢复协议。
官方资料:https://pkg.go.dev/archive/zip#FileInfoHeader、https://pkg.go.dev/archive/zip#FileHeader.SetMode、https://pkg.go.dev/archive/zip#Writer.CreateHeader、https://pkg.go.dev/archive/zip#Writer.Close。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
116 收藏
-
170 收藏
-
477 收藏
-
419 收藏
-
408 收藏
-
430 收藏
-
136 收藏
-
463 收藏
-
Golang · Go教程 | 27分钟前 | Go教程 · database/sql · database/sql Go数据库 sql.Stmt PrepareContext Conn.PrepareContext123 收藏
-
209 收藏
-
281 收藏
-
304 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习