sevenzip
| Language | Go |
|---|---|
| License | BSD-3-Clause |
| Repository | github.com/bodgit/sevenzip |
| Documentation | pkg.go.dev/github.com/bodgit/sevenzip |
| Depends on | pure Go (klauspost/compress, ulikunitz/xz and others) |
| Read / write | yes / no (LZMA, LZMA2, Deflate, BZip2, PPMd, Zstd, Brotli, LZ4, Copy, BCJ filters; encrypted, multi-volume) |
sevenzip is a pure Go 7z reader modeled on archive/zip: sevenzip.OpenReader(path) returns a reader whose File slice lists every entry, and f.Open() returns the decompressed data. The reader implements fs.FS. It handles compressed and encrypted headers, multi-volume archives and every common 7z method.
7z archives are usually solid: files share one compressed stream. sevenzip caches the stream decoder, so reading files in archive order and closing each one before opening the next continues decoding where the last file ended. Opening files in random order restarts from the beginning of the block each time.
Installation#
go get github.com/bodgit/sevenzip
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, and a natural, case-insensitive compare.
package main
import (
"fmt"
"log"
"os"
"slices"
"github.com/bodgit/sevenzip"
)
func pages(r *sevenzip.Reader) []*sevenzip.File {
var res []*sevenzip.File
for _, f := range r.File {
if !f.FileInfo().IsDir() && isPage(f.Name) {
res = append(res, f)
}
}
slices.SortFunc(res, func(a, b *sevenzip.File) int { return naturalCompare(a.Name, b.Name) })
return res
}
func main() {
r, err := sevenzip.OpenReader(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer r.Close()
for i, f := range pages(&r.Reader) {
fmt.Printf("%3d %s (%d bytes)\n", i+1, f.Name, f.UncompressedSize)
}
}
How to read all pages into memory#
Iterate r.File in archive order (not page order) so the decoder is reused, and close each file before the next:
func readFile(f *sevenzip.File) ([]byte, error) {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close() // must close before opening the next file
return io.ReadAll(rc)
}
func readPages(path string) (map[string][]byte, error) {
r, err := sevenzip.OpenReader(path)
if err != nil {
return nil, err
}
defer r.Close()
data := map[string][]byte{}
for _, f := range r.File { // archive order
if f.FileInfo().IsDir() || !isPage(f.Name) {
continue
}
b, err := readFile(f)
if err != nil {
return nil, err
}
data[f.Name] = b
}
return data, nil
}
How to extract the cover#
r, err := sevenzip.OpenReader("comic.cb7")
if err != nil {
log.Fatal(err)
}
defer r.Close()
if ps := pages(&r.Reader); len(ps) > 0 {
b, err := readFile(ps[0])
if err != nil {
log.Fatal(err)
}
os.WriteFile("cover"+strings.ToLower(path.Ext(ps[0].Name)), b, 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(r *sevenzip.Reader) (*ComicInfo, error) {
for _, f := range r.File {
if strings.EqualFold(f.Name, "ComicInfo.xml") {
b, err := readFile(f)
if err != nil {
return nil, err
}
var ci ComicInfo
return &ci, xml.Unmarshal(b, &ci)
}
}
return nil, nil
}
How to convert CB7 to CBZ#
func cb7ToCbz(in, out string) error {
r, err := sevenzip.OpenReader(in)
if err != nil {
return err
}
defer r.Close()
f, err := os.Create(out)
if err != nil {
return err
}
defer f.Close()
zw := zip.NewWriter(f)
for _, sf := range r.File {
if sf.FileInfo().IsDir() {
continue
}
b, err := readFile(sf)
if err != nil {
return err
}
w, err := zw.CreateHeader(&zip.FileHeader{Name: sf.Name, Method: zip.Store, Modified: sf.Modified})
if err != nil {
return err
}
if _, err := w.Write(b); err != nil {
return err
}
}
return zw.Close()
}
Tips#
- Password-protected archives:
sevenzip.OpenReaderWithPassword(path, "secret"). sevenzip.NewReader(readerAt, size)reads an archive from memory or anyio.ReaderAt.- Pass a name ending in
.001toOpenReaderto open a multi-volume set.