mholt/archives

GitHub: mholt/archives

一个纯 Go 实现的跨平台归档与压缩处理库,提供统一的流式 API 和虚拟文件系统抽象,支持多种归档和压缩格式的创建、提取、识别与遍历。

Stars: 379 | Forks: 33

# archives [![Go Reference](https://pkg.go.dev/badge/github.com/mholt/archives.svg)](https://pkg.go.dev/github.com/mholt/archives) [![Linux](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/6bdb4ce4fd094435.svg)](https://github.com/mholt/archives/actions/workflows/ubuntu-latest.yml) [![Mac](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/abfe39fcf5094436.svg)](https://github.com/mholt/archives/actions/workflows/macos-latest.yml) [![Windows](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/4d8d9b73a0094437.svg)](https://github.com/mholt/archives/actions/workflows/windows-latest.yml) 介绍 **mholt/archives** —— 这是一个跨平台、多格式的 Go 库,用于通过统一的 API 处理归档和压缩格式,并作为兼容 [`io/fs`](https://pkg.go.dev/io/fs) 的虚拟文件系统。 ## 功能特性 - 流式 API - 自动识别归档和压缩格式: - 根据文件名 - 根据流 peek(文件头) - 将目录、归档和其他文件作为 [`io/fs`](https://pkg.go.dev/io/fs) 文件系统统一遍历: - [`FileFS`](https://pkg.go.dev/github.com/mholt/archives#FileFS) - [`DirFS`](https://pkg.go.dev/github.com/mholt/archives#DirFS) - [`ArchiveFS`](https://pkg.go.dev/github.com/mholt/archives#ArchiveFS) - 使用 [`DeepFS`](https://pkg.go.dev/github.com/mholt/archives#DeepFS) 无缝进入归档文件内部 - 压缩和解压文件 - 创建和提取归档文件 - 遍历或进入归档文件 - 仅从归档中提取特定文件 - 向 .tar 和 .zip 归档插入(追加)内容,无需重新创建整个归档 - 支持多种归档和压缩格式 - 读取受密码保护的 7-Zip 和 RAR 文件 - 可扩展(只需注册即可添加更多格式) - 跨平台,静态二进制文件 - 纯 Go(无 cgo) - 多线程 Gzip - 可调节压缩级别 - 超快的 Snappy 实现(通过 [S2](https://github.com/klauspost/compress/blob/master/s2/README.md)) ### 支持的压缩格式 - brotli (.br) - bzip2 (.bz2) - flate (.zip) - gzip (.gz) - lz4 (.lz4) - lzip (.lz) - minlz (.mz) - snappy (.sz) 和 S2 (.s2) - xz (.xz) - zlib (.zz) - zstandard (.zst) ### 支持的归档格式 - .zip - .tar(包括任何压缩变体,如 .tar.gz) - .rar(只读) - .7z(只读) ## 命令行工具 目前有一个独立维护的命令行工具 [**`arc`**](https://github.com/jm33-m0/arc) 正在开发中,它将把此库的许多功能暴露给 Shell。 ## 库的使用 ``` $ go get github.com/mholt/archives ``` ### 创建归档 创建归档完全可以不需要真正的磁盘或存储设备。你只需要一个 [`FileInfo` structs](https://pkg.go.dev/github.com/mholt/archives#FileInfo) 列表,这可以在没有真实文件系统的情况下实现。 不过,从磁盘创建归档非常常见,因此你可以使用 [`FilesFromDisk()` function](https://pkg.go.dev/github.com/mholt/archives#FilesFromDisk) 来帮助你将磁盘上的文件名映射到它们在归档中的路径。 在此示例中,我们将 4 个文件和一个目录(递归包括其内容)添加到 .tar.gz 文件中: ``` ctx := context.TODO() // map files on disk to their paths in the archive using default settings (second arg) files, err := archives.FilesFromDisk(ctx, nil, map[string]string{ "/path/on/disk/file1.txt": "file1.txt", "/path/on/disk/file2.txt": "subfolder/file2.txt", "/path/on/disk/file3.txt": "", // put in root of archive as file3.txt "/path/on/disk/file4.txt": "subfolder/", // put in subfolder as file4.txt "/path/on/disk/folder": "Custom Folder", // contents added recursively }) if err != nil { return err } // create the output file we'll write to out, err := os.Create("example.tar.gz") if err != nil { return err } defer out.Close() // we can use the CompressedArchive type to gzip a tarball // (since we're writing, we only set Archival, but if you're // going to read, set Extraction) format := archives.CompressedArchive{ Compression: archives.Gz{}, Archival: archives.Tar{}, } // create the archive err = format.Archive(ctx, out, files) if err != nil { return err } ``` ### 提取归档 提取归档、从归档中提取文件以及遍历归档都是同一个函数。 只需使用你的格式类型(例如 `Zip`)调用 `Extract()`。你将传入一个上下文(用于取消)、输入流和一个回调函数来处理每个文件。 ``` // the type that will be used to read the input stream var format archives.Zip err := format.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error { // do something with the file here; or, if you only want a specific file or directory, // just return until you come across the desired f.NameInArchive value(s) return nil }) if err != nil { return err } ``` ### 识别格式 当你有一个内容未知的输入流时,此包可以为你识别它。它将尝试根据文件名和/或文件头(peek 流)进行匹配: ``` // unless your stream is an io.Seeker, use the returned stream value to // ensure you re-read the bytes consumed during Identify() format, stream, err := archives.Identify(ctx, "filename.tar.zst", stream) if err != nil { return err } // you can now type-assert format to whatever you need // want to extract something? if ex, ok := format.(archives.Extractor); ok { // ... proceed to extract } // or maybe it's compressed and you want to decompress it? if decomp, ok := format.(archives.Decompressor); ok { rc, err := decomp.OpenReader(unknownFile) if err != nil { return err } defer rc.Close() // read from rc to get decompressed data } ``` `Identify()` 通过从流的开头读取任意数量的字节(刚好足以检查文件头)来工作。它会缓冲这些字节并返回一个新的 reader,让你可以重新从头读取它们。但是,如果你的输入流是 `io.Seeker`,则不会创建缓冲区,因为它使用 `Seek()` 代替,并且返回的流与输入相同。 ### 虚拟文件系统 这是我最喜欢的功能。 假设你在磁盘上有一个目录、一个归档、一个压缩归档、任何其他常规文件,或者上述任何一种的流!你并不真正在意;你只想无论它是什么都能统一使用它。 只需创建一个文件系统: ``` // filename could be: // - a folder ("/home/you/Desktop") // - an archive ("example.zip") // - a compressed archive ("example.tar.gz") // - a regular file ("example.txt") // - a compressed regular file ("example.txt.gz") // and/or the last argument could be a stream of any of the above fsys, err := archives.FileSystem(ctx, filename, nil) if err != nil { return err } ``` 这是一个功能齐全的 `fs.FS`,因此你可以打开文件和读取目录,无论输入是什么类型的文件。 例如,要打开特定文件: ``` f, err := fsys.Open("file") if err != nil { return err } defer f.Close() ``` 如果你打开了一个常规文件或归档,你可以从中读取。如果是压缩文件,读取时会自动解压。 如果你打开了一个目录(无论是真实的还是在归档中),你可以列出其内容: ``` if dir, ok := f.(fs.ReadDirFile); ok { // 0 gets all entries, but you can pass > 0 to paginate entries, err := dir.ReadDir(0) if err != nil { return err } for _, e := range entries { fmt.Println(e.Extension()) } } ``` 或者通过这种方式获取目录列表: ``` entries, err := fsys.ReadDir("Playlists") if err != nil { return err } for _, e := range entries { fmt.Println(e.Extension()) } ``` 或者你可能希望遍历文件系统的全部或部分,但跳过名为 `.git` 的文件夹: ``` err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if path == ".git" { return fs.SkipDir } fmt.Println("Walking:", path, "Dir?", d.IsDir()) return nil }) if err != nil { return err } ``` `archives` 包可以让你完成所有这些操作。 **重要的 .tar 说明:** Tar 文件无法高效实现文件系统语义,因为它们历史上源于磁带的顺序访问设计。文件系统固有地假设存在某种索引来促进随机访问,但 tar 文件需要从头开始读取才能访问末尾的内容。当归档被压缩时,这尤其缓慢。已经实施了优化以分摊 `ReadDir()` 调用,使得 `fs.WalkDir()` 只需扫描归档一次,但这会使用更多内存。Open 调用需要再次扫描才能找到文件。如果文件系统语义对你来说不重要,直接使用 `Tar.Extract()` 可能会更高效。 #### 与 `http.FileServer` 配合使用 它可以与 http.FileServer 配合使用,以便在浏览器中浏览归档和目录。但是,由于 http.FileServer 的工作方式,不要直接将 http.FileServer 与压缩文件一起使用;而是像下面这样进行包装: ``` fileServer := http.FileServer(http.FS(archiveFS)) http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { // disable range request writer.Header().Set("Accept-Ranges", "none") request.Header.Del("Range") // disable content-type sniffing ctype := mime.TypeByExtension(filepath.Ext(request.URL.Path)) writer.Header()["Content-Type"] = nil if ctype != "" { writer.Header().Set("Content-Type", ctype) } fileServer.ServeHTTP(writer, request) }) ``` 如果无法从文件名推断,http.FileServer 默认会尝试嗅探 Content-Type。为此,http 包会尝试从文件中读取,然后 Seek 回文件开头,这是该库目前无法实现的。Range 请求也是如此。由于依赖项的限制,此包目前不支持在归档内进行 Seeking。 如果需要 Content-Type,你可以自己[注册它](https://pkg.go.dev/mime#AddExtensionType)。 ### 压缩数据 压缩格式允许你打开 writer 来压缩数据: ``` // wrap underlying writer w compressor, err := archives.Zstd{}.OpenWriter(w) if err != nil { return err } defer compressor.Close() // writes to compressor will be compressed ``` ### 解压数据 同样,压缩格式允许你打开 reader 来解压数据: ``` // wrap underlying reader r decompressor, err := archives.Snappy{}.OpenReader(r) if err != nil { return err } defer decompressor.Close() // reads from decompressor will be decompressed ``` ### 追加到 tarball 和 zip 归档 通过在 tar 或 zip 流上调用 `Insert()`,可以追加到 Tar 和 Zip 归档,而无需创建一个全新的归档。但是,对于 tarball,这要求 tarball 未被压缩(由于修改压缩字典的复杂性)。 这是一个将文件追加到磁盘上的 tarball 的示例: ``` tarball, err := os.OpenFile("example.tar", os.O_RDWR, 0644) if err != nil { return err } defer tarball.Close() // prepare a text file for the root of the archive files, err := archives.FilesFromDisk(nil, map[string]string{ "/home/you/lastminute.txt": "", }) err := archives.Tar{}.Insert(context.Background(), tarball, files) if err != nil { return err } ``` 插入到 Zip 归档的代码类似,只是你将在 `Zip{}` 值上调用 `Insert()`。 ### 遍历时进入归档 如果你正在使用 [`fs.WalkDir()`](https://pkg.go.dev/io/fs#WalkDir) 遍历/行走文件系统,[**`DeepFS`**](https://pkg.go.dev/github.com/mholt/archives#DeepFS) 类型允许你透明地遍历归档(和压缩归档!)的内容,就像归档文件是磁盘上的常规目录一样。 只需将你的 DeepFS 根植于真实路径,然后开始遍历: ``` fsys := &archives.DeepFS{Root: "/some/dir"} err := fs.WalkDir(fsys, ".", func(fpath string, d fs.DirEntry, err error) error { ... }) ``` 你会注意到归档内的路径看起来像 `/some/dir/archive.zip/foo/bar.txt`。如果你将这样的路径传递给 `fsys.Open()`,它会在归档文件(`/some/dir/archive.zip`)的末尾拆分路径,并在归档内部使用路径的其余部分(`foo/bar.txt`)。
标签:7-Zip, EVTX分析, EVTX分析, EVTX分析, Go语言, Gzip, io/fs, mholt/archives, RAR, TAR, ZAP项目解析, ZIP, 压缩解压, 开发组件, 开源库, 归档工具, 搜索引擎爬虫, 数据压缩, 文件提取, 文件管理, 文件系统, 日志审计, 格式识别, 流式处理, 程序破解, 虚拟文件系统, 软件库, 静态二进制