登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

archive/zip 创建目录条目并保持文件权限的方法

来源:17golang原创

时间:2026-11-12 18:12:51 292浏览 收藏

Go 使用 archive/zip 创建带权限的目录和文件条目时,应先构造 FileHeader,再调用 CreateHeader。目录名称必须以 / 结尾且不写数据;权限用 SetMode 写入条目头,已有文件信息则可用 FileInfoHeader 生成初始元数据。归档结束必须检查 zip.Writer.Close 的错误,确保中央目录写入完成。

升级范围:从只写内容迁移到保留条目元数据

这里的升级指应用打包代码从“只保存路径与内容”迁移为“同时记录目录类型和权限”,不是某个 Go 新版本发布。本文保持源目录内部层级,显式记录空目录,并保存 ZIP 能表达的源模式位;不承诺把 ACL、扩展属性、属主和所有平台权限模型完整打包。

需要区分两件事:归档头里记录了什么,以及解压工具最终怎样创建文件。前者由生成器控制,后者还取决于解压实现、目标系统和策略。因此,“保持文件权限”应理解为正确记录相应元数据,并在使用环境里确认恢复行为。

变更表:把名称、类型和权限放回条目头

ZIP条目头名称权限压缩方式与内容约束结构图
结构图:ZIP 条目头关联名称、模式和压缩方式;目录条目不承载文件数据。
处理对象仅写内容的写法保留元数据的写法
普通文件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写入器与输出责任结构图
结构图:源文件句柄、ZIP 写入器和底层输出的关闭责任不同。

下面程序处理可信、稳定的普通目录,只接收普通文件和目录,遇到符号链接、设备等其他条目报错。示例先在内存中构建 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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>