archives (mholt/archives)
| Language | Go |
|---|---|
| License | MIT |
| Repository | github.com/mholt/archives |
| Documentation | pkg.go.dev/github.com/mholt/archives |
| Depends on | pure Go; uses bodgit/sevenzip, nwaples/rardecode and klauspost/compress |
| Read / write | yes / 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#
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.
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#
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:
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:
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:
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#
archives.FileSystemalso accepts a directory, so a reader can treat a folder of images and a CBZ the same way.- Entries come back in archive order from
Extract; always sort by name for page order. NameInArchiveis not sanitized: clean it withpath.Cleanand reject..before writing files to disk.