Overview of mholt/archives
mainio/fs package. Key capabilities include stream-oriented APIs, automatic format identification (via filename or stream peeking), and the ability to walk through archives seamlessly using DeepFS.repository·main·Indexed 19 days ago
https://github.com/mholt/archivesA Go library for handling various archive and compression formats through a unified API. It supports treating archives as io/fs compatible virtual file systems and provides automatic format identification. Supported compression formats include brotli, bzip2, flate, gzip, lz4, lzip, minlz, snappy, xz, zlib, and zstandard. Supported archive formats include .zip, .tar, .rar (read-only), and .7z (read-only).
io/fs package. Key capabilities include stream-oriented APIs, automatic format identification (via filename or stream peeking), and the ability to walk through archives seamlessly using DeepFS.The DeepFS type allows you to use fs.WalkDir() to traverse not just a directory, but also any archives (and compressed archives) contained within that directory, treating them as if they were regular folders.
// Root the DeepFS at a real path
fsys := &archives.DeepFS{Root: "/some/dir"}
err := fs.WalkDir(fsys, ".", func(fpath string, d fs.DirEntry, err error) error {
// Paths inside archives will look like: /some/dir/archive.zip/foo/bar.txt
return nil
})The archives.FileSystem() function creates a fully-featured fs.FS that provides uniform access to directories, regular files, archives, or compressed archives.
fsys.Open(name) to get an fs.File. If the source is compressed, reads are automatically decompressed.fsys.ReadDir(name) or cast an opened file to fs.ReadDirFile to call ReadDir(0).fs.WalkDir(fsys, ...) to traverse the structure.Performance Note for .tar: Tar files are sequential-access. While archives implements optimizations for ReadDir() and fs.WalkDir(), Open() calls may require a full scan of the archive. For high-performance extraction where file system semantics aren't needed, use Tar.Extract() directly.
// filename could be a folder, archive, compressed archive, or regular file
fsys, err := archives.FileSystem(ctx, filename, nil)
if err != nil {
return err
}
// Open a specific file
f, err := fsys.Open("file")
if err != nil {
return err
}
defer f.Close()
// List contents of a directory
entries, err := fsys.ReadDir("Playlists")
if err != nil {
return err
}
// Walk the file system
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
return nil
})To use the archives library in your Go project, install it using go get:
$ go get github.com/mholt/archivesYou can create archives using FilesFromDisk() to map local files to paths within the archive. To create a compressed archive (like .tar.gz), use the CompressedArchive type, which combines an Archival format (e.g., Tar) and a Compression format (e.g., 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
format := archives.CompressedArchive{
Compression: archives.Gz{},
Archival: archives.Tar{},
}
// create the archive
err = format.Archive(ctx, out, files)
if err != nil {
return err
}Tar and Zip archives support appending files without recreating the entire archive using the Insert() method.
Constraint: For tarballs, the archive must not be compressed (e.g., use .tar, not .tar.gz), as modifying compression dictionaries is too complex.
tarball, err := os.OpenFile("example.tar", os.O_RDWR, 0644)
if err != nil {
return err
}
defer tarball.Close()
// prepare files to insert
files, err := archives.FilesFromDisk(nil, map[string]string{
"/home/you/lastminute.txt": "",
})
// insert into the tarball
err := archives.Tar{}.Insert(context.Background(), tarball, files)
if err != nil {
return err
}Use archives.Identify() to detect the format of an input stream based on filename and/or file headers.
Note: If your input stream is not an io.Seeker, you must use the returned stream value to ensure you can re-read the bytes consumed during identification. If it is an io.Seeker, the returned stream is the same as the input.
Once identified, you can type-assert the returned format to interfaces like archives.Extractor or archives.Decompressor to perform operations.
// 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
}To extract an archive, use the Extract() method on your format type (e.g., Zip). You must provide a context, an input stream, and a callback function that receives each FileInfo. The callback is used to process individual files or directories.
// 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
}To browse archives or directories in a web browser using http.FileServer, you must wrap the archiveFS to handle limitations regarding Seek() and Content-Type sniffing. Because the library cannot currently support Seek() in archives, you must disable range requests and manually manage Content-Type headers.
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)
})A MatchResult indicates how a format was identified. A match is successful if it is identified by either the filename or the data stream.
ByName: The format matched based on the file extension.ByStream: The format matched based on the content (e.g., magic bytes/headers).A ByStream match is generally considered stronger and more reliable than a ByName match.
type MatchResult struct {
ByName, ByStream bool
}
func (mr MatchResult) Matched() boolA CompressedArchive is a composite type representing an archive that is wrapped in a compression layer (e.g., a .tar.gz file). It implements multiple interfaces to provide transparent operations:
Limitations: Because of the complexities of modifying existing compression states, files cannot be inserted or appended to a CompressedArchive.
type CompressedArchive struct {
Archival
Extraction
Compression
}When an archive is extracted to disk, it may not create a new top-level container folder. This causes a discrepancy where a file path like a/b/c inside an archive becomes b/c on disk (if a was the root of the extraction).
To write code that works uniformly whether you are accessing the archive directly or an extracted directory, use the TopDir* functions. These functions attempt to open/stat/read the path as provided; if that fails, they automatically try the path without its first element (e.g., if a/b/c fails, they try b/c).
Note: These functions are EXPERIMENTAL and subject to change or removal.
import (
"io/fs"
"github.com/mholt/archives"
)
// Example usage of TopDirOpen
file, err := archives.TopDirOpen(myFsys, "a/b/c")
if err != nil {
// handle error
}
// Other available TopDir functions:
// archives.TopDirStat(fsys, name) -> (fs.FileInfo, error)
// archives.TopDirReadDir(fsys, name) -> ([]fs.DirEntry, error)