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 / yes (ZIP and TAR read/write; RAR and 7z read only) |
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 whether the comic is a CBZ, CBR or CB7. The format is detected from the content, so a .cbr that is really ZIP still opens.
The library is pre-1.0 (v0.1.x): the API may still change between minor versions. If you only handle CBZ, the standard archive/zip has no dependencies.
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) // CBZ, CBR, CB7 or CBT
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 create a CBZ from a folder of images#
FilesFromDisk maps files on disk to names in the archive. SelectiveCompression stores files that are already compressed (JPEG, PNG, by extension) and deflates the rest:
func makeCbz(ctx context.Context, dir, out string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
var names []string
for _, e := range entries {
if e.Type().IsRegular() && isPage(e.Name()) {
names = append(names, e.Name())
}
}
slices.SortFunc(names, naturalCompare)
m := map[string]string{}
for i, n := range names {
m[filepath.Join(dir, n)] = fmt.Sprintf("%03d%s", i+1, strings.ToLower(path.Ext(n)))
}
if _, err := os.Stat(filepath.Join(dir, "ComicInfo.xml")); err == nil {
m[filepath.Join(dir, "ComicInfo.xml")] = "ComicInfo.xml"
}
files, err := archives.FilesFromDisk(ctx, nil, m)
if err != nil {
return err
}
f, err := os.Create(out)
if err != nil {
return err
}
defer f.Close()
return archives.Zip{SelectiveCompression: true}.Archive(ctx, f, files)
}
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 or 7z, and it works on a non-seekable stream for TAR:
f, _ := os.Open("comic.cbz")
defer f.Close()
format, stream, err := archives.Identify(ctx, "comic.cbz", 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.