archive/tar
| Language | Go |
|---|---|
| License | BSD-3-Clause (Go standard library) |
| Repository | go.googlesource.com/go |
| Documentation | pkg.go.dev/archive/tar |
| Depends on | nothing (standard library) |
| Read / write | yes / yes (ustar, pax, GNU) |
archive/tar reads and writes TAR archives as streams. tar.NewReader(r) returns a reader; Next() advances to the next header and the reader then returns that file’s data. tar.NewWriter(w) writes headers and data. It handles pax and GNU extensions, so long and non-ASCII names work.
There’s no random access: to read a page, walk the headers until you reach it. On an *os.File, the reader seeks over data you skip, so listing a CBT is fast.
Installation#
Nothing to install: import "archive/tar".
Basic usage: list pages in natural order#
isPage and naturalCompare are the helpers from the archive/zip CBZ page: an image-extension filter that skips __MACOSX and dotfiles (including macOS ._ files), and a natural, case-insensitive compare.
package main
import (
"archive/tar"
"errors"
"fmt"
"io"
"log"
"os"
"slices"
)
func listPages(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
tr := tar.NewReader(f)
var pages []string
for {
h, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if h.Typeflag == tar.TypeReg && isPage(h.Name) {
pages = append(pages, h.Name)
}
}
slices.SortFunc(pages, naturalCompare)
return pages, nil
}
func main() {
pages, err := listPages(os.Args[1])
if err != nil {
log.Fatal(err)
}
for i, p := range pages {
fmt.Printf("%3d %s\n", i+1, p)
}
}
How to read pages into memory#
Read the pages you need in one pass:
func readFiles(path string, want map[string]bool) (map[string][]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
tr := tar.NewReader(f)
out := map[string][]byte{}
for len(out) < len(want) {
h, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if want[h.Name] {
b, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
out[h.Name] = b
}
}
return out, nil
}
How to extract the cover#
pages, err := listPages("comic.cbt")
if err != nil || len(pages) == 0 {
log.Fatal("no pages")
}
files, err := readFiles("comic.cbt", map[string]bool{pages[0]: true})
if err != nil {
log.Fatal(err)
}
os.WriteFile("cover"+strings.ToLower(path.Ext(pages[0])), files[pages[0]], 0o644)
How to read ComicInfo.xml#
type ComicInfo struct {
Title, Series, Number, Writer, Manga string
PageCount int
Pages []struct {
Image int `xml:"Image,attr"` // index in sorted page order
Type string `xml:"Type,attr"`
} `xml:"Pages>Page"`
}
func readComicInfo(path string) (*ComicInfo, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
tr := tar.NewReader(f)
for {
h, err := tr.Next()
if errors.Is(err, io.EOF) {
return nil, nil
}
if err != nil {
return nil, err
}
if strings.EqualFold(h.Name, "ComicInfo.xml") {
var ci ComicInfo
return &ci, xml.NewDecoder(tr).Decode(&ci)
}
}
}
How to create a CBT from a folder of images#
tar.FileInfoHeader builds a header from os.FileInfo; set Name to the name inside the archive:
func makeCbt(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)
f, err := os.Create(out)
if err != nil {
return err
}
defer f.Close()
tw := tar.NewWriter(f)
add := func(src, name string) error {
fi, err := os.Stat(filepath.Join(dir, src))
if err != nil {
return err
}
h, err := tar.FileInfoHeader(fi, "")
if err != nil {
return err
}
h.Name = name
h.Uname, h.Gname = "", "" // don't leak local user names
if err := tw.WriteHeader(h); err != nil {
return err
}
in, err := os.Open(filepath.Join(dir, src))
if err != nil {
return err
}
defer in.Close()
_, err = io.Copy(tw, in)
return err
}
for i, n := range names {
if err := add(n, fmt.Sprintf("%03d%s", i+1, strings.ToLower(path.Ext(n)))); err != nil {
return err
}
}
if _, err := os.Stat(filepath.Join(dir, "ComicInfo.xml")); err == nil {
if err := add("ComicInfo.xml", "ComicInfo.xml"); err != nil {
return err
}
}
return tw.Close() // writes the two zero blocks that end the archive
}
Go 1.22+ has tw.AddFS(os.DirFS(dir)), which adds a whole folder in one call when the names are already right.
Tips#
- Check
h.Typeflag == tar.TypeReg: pax headers are consumed by the reader, but directories and links also appear as entries. h.Nameis not sanitized; clean it and reject..before writing to disk.- To serve pages from a large uncompressed CBT, scan once and record each page’s data offset with
f.Seek(0, io.SeekCurrent)right afterNext()returns (the reader doesn’t buffer ahead), then read a page later withio.NewSectionReader(f, offset, h.Size)without rescanning.