archives

repository·main·Indexed 19 days ago

https://github.com/mholt/archives

A 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).

Tokens
18.6K
Snippets
78
Records
113
Agent score
65%

What's inside archives

  1. Overview of mholt/archives

    main
    mholt/archives is a cross-platform, multi-format Go library designed for working with archives and compression formats. It provides a unified API and supports treating archives as virtual file systems compatible with Go's 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.
  2. Walk through archives transparently with DeepFS

    main

    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
    })
  3. Use Virtual File Systems (fs.FS) for uniform access

    main

    The archives.FileSystem() function creates a fully-featured fs.FS that provides uniform access to directories, regular files, archives, or compressed archives.

    • Opening files: Use fsys.Open(name) to get an fs.File. If the source is compressed, reads are automatically decompressed.
    • Listing directories: Use fsys.ReadDir(name) or cast an opened file to fs.ReadDirFile to call ReadDir(0).
    • Walking: Use standard 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
    })
  4. Create an archive from disk

    main

    You 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
    }
  5. Append files to Tar and Zip archives

    main

    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
    }
  6. Identify unknown archive or compression formats

    main

    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
    }
  7. Extract an archive

    main

    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
    }
  8. Serve archives via http.FileServer

    main

    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)
    })
  9. Understand MatchResult

    main

    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() bool
  10. How CompressedArchive works

    main

    A 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:

    • Archival/Extraction: It can archive files into the format or extract files from it.
    • Compression/Decompression: It handles the underlying compression layer.
    • Matching: It matches if both the compression and the archival/extraction layers match the input.

    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
    }
  11. Use TopDir functions to handle path differences between archives and extracted directories

    main

    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)