All formats

archives (mholt/archives)

LanguageGo
LicenseMIT
Repositorygithub.com/mholt/archives
Documentationpkg.go.dev/github.com/mholt/archives
Depends onpure Go; uses bodgit/sevenzip, nwaples/rardecode and klauspost/compress
Read / writeyes / no for RAR (reads RAR4 and RAR5, solid; writes only ZIP and TAR)

archives (the successor of mholt/archiver) gives one API for ZIP, TAR, RAR and 7z. archives.FileSystem opens a file and returns an fs.FS, so you list and read pages with the standard fs.WalkDir and fs.ReadFile. For CBR files it uses nwaples/rardecode underneath and reads RAR (RAR4 and RAR5, solid). The same code opens CBZ and CB7, and the format is detected from the content, so a misnamed file still opens.

It can’t create RAR archives: convert to CBZ, shown below. The library is pre-1.0 (v0.1.x), so the API may change between minor versions. For a smaller dependency, use rardecode directly.

Installation#

Shell
go get github.com/mholt/archives

Basic usage: list pages in natural order#

isPage and naturalCompare are the helpers from the archive/zip page: an image-extension filter that skips __MACOSX and dotfiles, and a natural, case-insensitive compare.

Go
package main

import (
	"context"
	"fmt"
	"io/fs"
	"log"
	"os"
	"slices"

	"github.com/mholt/archives"
)

func listPages(fsys fs.FS) ([]string, error) {
	var pages []string
	err := fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() && isPage(p) {
			pages = append(pages, p)
		}
		return nil
	})
	slices.SortFunc(pages, naturalCompare)
	return pages, err
}

func main() {
	ctx := context.Background()
	fsys, err := archives.FileSystem(ctx, os.Args[1], nil) // e.g. "comic.cbr"
	if err != nil {
		log.Fatal(err)
	}
	pages, err := listPages(fsys)
	if err != nil {
		log.Fatal(err)
	}
	for i, p := range pages {
		fmt.Printf("%3d %s\n", i+1, p)
	}
}

How to extract the cover#

Go
pages, err := listPages(fsys)
if err == nil && len(pages) > 0 {
	data, err := fs.ReadFile(fsys, pages[0])
	if err != nil {
		log.Fatal(err)
	}
	os.WriteFile("cover"+path.Ext(pages[0]), data, 0o644)
}

How to read ComicInfo.xml#

The name’s case varies between files, so look it up in the root directory listing:

Go
type ComicInfo struct {
	Title, Series, Number, Writer, Manga string
	PageCount                            int
	Pages                                []struct {
		Image int    `xml:"Image,attr"`
		Type  string `xml:"Type,attr"`
	} `xml:"Pages>Page"`
}

func readComicInfo(fsys fs.FS) (*ComicInfo, error) {
	entries, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return nil, err
	}
	for _, e := range entries {
		if strings.EqualFold(e.Name(), "ComicInfo.xml") {
			data, err := fs.ReadFile(fsys, e.Name())
			if err != nil {
				return nil, err
			}
			var ci ComicInfo
			return &ci, xml.Unmarshal(data, &ci)
		}
	}
	return nil, nil
}

How to convert CBR to CBZ#

Walk the archive and copy every file into a ZIP with stored entries; pages are not re-encoded:

Go
func toCbz(ctx context.Context, in, out string) error {
	fsys, err := archives.FileSystem(ctx, in, nil)
	if err != nil {
		return err
	}
	f, err := os.Create(out)
	if err != nil {
		return err
	}
	defer f.Close()
	zw := zip.NewWriter(f)
	err = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return err
		}
		data, err := fs.ReadFile(fsys, p)
		if err != nil {
			return err
		}
		w, err := zw.CreateHeader(&zip.FileHeader{Name: p, Method: zip.Store, Modified: time.Now()})
		if err != nil {
			return err
		}
		_, err = w.Write(data)
		return err
	})
	if err != nil {
		return err
	}
	return zw.Close()
}

How to stream through an archive without fs.FS#

Extract calls a function for every entry in archive order. This is the fastest way to read all pages of a solid RAR archive:

Go
f, _ := os.Open("comic.cbr")
defer f.Close()
format, stream, err := archives.Identify(ctx, "comic.cbr", f)
if err != nil {
	log.Fatal(err)
}
ex, ok := format.(archives.Extractor)
if !ok {
	log.Fatal("not an archive")
}
err = ex.Extract(ctx, stream, func(ctx context.Context, fi archives.FileInfo) error {
	if fi.IsDir() || !isPage(fi.NameInArchive) {
		return nil
	}
	rc, err := fi.Open()
	if err != nil {
		return err
	}
	defer rc.Close()
	data, err := io.ReadAll(rc)
	fmt.Println(fi.NameInArchive, len(data))
	return err
})

Tips#