From 7440b0d5a27992cab532260714361940ffbe2be8 Mon Sep 17 00:00:00 2001 From: David Newhall Date: Sat, 5 Jun 2021 01:44:15 -0700 Subject: [PATCH] Add 7z support (#6) * Add 7z support * add exmaple app --- .gitignore | 13 +++++++++ .travis.yml | 2 +- 7z.go | 63 +++++++++++++++++++++++++++++++++++++++++ Makefile | 10 ++++--- README.md | 6 ++-- cmd/xt/README.md | 7 +++++ cmd/xt/main.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ files.go | 4 ++- go.mod | 7 ++++- go.sum | 6 ++++ rar.go | 2 +- 11 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 100644 7z.go create mode 100644 cmd/xt/README.md create mode 100644 cmd/xt/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b99828 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*~ +*.zip +*.rar +*.r00 +*.r01 +*.7z +*.tar +*.gz +*.tgz +*.bz2 +*.tbz2 +/cmd/xt/xt +/xt diff --git a/.travis.yml b/.travis.yml index e431dbd..03ef07f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,6 @@ language: go go: - 1.16.x install: - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.40.1 script: - make test diff --git a/7z.go b/7z.go new file mode 100644 index 0000000..0362582 --- /dev/null +++ b/7z.go @@ -0,0 +1,63 @@ +package xtractr + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/saracen/go7z" +) + +// Extract7z extracts a 7zip archive. This wraps https://github.com/saracen/go7z. +func Extract7z(x *XFile) (int64, []string, error) { + sz, err := go7z.OpenReader(x.FilePath) + if err != nil { + return 0, nil, fmt.Errorf("os.Open: %w", err) + } + defer sz.Close() + + return x.un7zip(sz) +} + +func (x *XFile) un7zip(szreader *go7z.ReadCloser) (int64, []string, error) { + files := []string{} + size := int64(0) + + for { + header, err := szreader.Next() + + switch { + case errors.Is(err, io.EOF): + return size, files, nil + case err != nil: + return size, files, fmt.Errorf("szreader.Next: %w", err) + case header == nil: + return size, files, fmt.Errorf("%w: %s", ErrInvalidHead, x.FilePath) + } + + wfile := x.clean(header.Name) + if !strings.HasPrefix(wfile, x.OutputDir) { + // The file being written is trying to write outside of our base path. Malicious archive? + return size, files, fmt.Errorf("%s: %w: %s (from: %s)", x.FilePath, ErrInvalidPath, wfile, header.Name) + } + + // https://github.com/saracen/go7z/blob/9c09b6bd7fda869ef48ff6f693744a65f477816b/README.md#usage + if header.IsEmptyStream && !header.IsEmptyFile { + if err = os.MkdirAll(wfile, x.DirMode); err != nil { + return size, files, fmt.Errorf("os.MkdirAll: %w", err) + } + + continue + } + + s, err := writeFile(wfile, szreader, x.FileMode, x.DirMode) + if err != nil { + return size, files, err + } + + files = append(files, wfile) + size += s + } +} diff --git a/Makefile b/Makefile index 6f2faa8..7409366 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,12 @@ test: lint go test -race -covermode=atomic ./... # Test 32 bit OSes. GOOS=linux GOARCH=386 go build . + GOOS=windows GOARCH=386 go build . GOOS=freebsd GOARCH=386 go build . lint: - GOOS=linux golangci-lint run --enable-all -D nlreturn,exhaustivestruct - GOOS=darwin golangci-lint run --enable-all -D nlreturn,exhaustivestruct - GOOS=windows golangci-lint run --enable-all -D nlreturn,exhaustivestruct - GOOS=freebsd golangci-lint run --enable-all -D nlreturn,exhaustivestruct + golangci-lint --version + GOOS=linux golangci-lint run --enable-all -D nlreturn,exhaustivestruct,interfacer,golint,scopelint,maligned + GOOS=darwin golangci-lint run --enable-all -D nlreturn,exhaustivestruct,interfacer,golint,scopelint,maligned + GOOS=windows golangci-lint run --enable-all -D nlreturn,exhaustivestruct,interfacer,golint,scopelint,maligned + GOOS=freebsd golangci-lint run --enable-all -D nlreturn,exhaustivestruct,interfacer,golint,scopelint,maligned diff --git a/README.md b/README.md index a12c6f8..99c3cf9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # `xtractr` -Go Library for Queuing and Extracting ZIP, RAR, GZ, BZ2, TAR, TGZ, TBZ2 files. +Go Library for Queuing and Extracting ZIP, RAR, GZ, BZ2, TAR, TGZ, TBZ2, 7Z files. Can also be used ad-hoc for direct decompression and extraction. See docs. - [GoDoc](https://pkg.go.dev/golift.io/xtractr) - +- Works on Linux, Windows, FreeBSD and macOS **without Cgo**. +- Supports 32 and 64 bit architectures. # Examples @@ -96,6 +97,7 @@ know the file type you may call the direct method instead: - `ExtractBzip(*XFile)` - `ExtractTarGzip(*XFile)` - `ExtractTarBzip(*XFile)` + - `Extract7z(*XFile)` ```golang package main diff --git a/cmd/xt/README.md b/cmd/xt/README.md new file mode 100644 index 0000000..c55796c --- /dev/null +++ b/cmd/xt/README.md @@ -0,0 +1,7 @@ +# xt + +This is an example app you may compile and use to extract files or whole directories. + +```shell +go get -u golift.io/xtractr/cmd/xt +``` diff --git a/cmd/xt/main.go b/cmd/xt/main.go new file mode 100644 index 0000000..1dab45d --- /dev/null +++ b/cmd/xt/main.go @@ -0,0 +1,74 @@ +// Package main is a binary used for demonstration purposes. It works, but lacks +// the features you can program into your own application. This is just a quick +// sample provided to show one way to interface this library. +package main + +import ( + "flag" + "log" + "os" + "strings" + "time" + + "golift.io/xtractr" +) + +func main() { + pwd, _ := os.Getwd() + output := flag.String("output", pwd, "Output directory, default is current directory") + + flag.Parse() + log.SetFlags(0) + + inputFiles := flag.Args() + if len(inputFiles) < 1 { + log.Printf("If you pass a directory, this app will extract every archive in it.") + log.Fatalf("Usage: %s [-output ] [paths...]", os.Args[0]) + } + + processInput(inputFiles, *output) +} + +func processInput(paths []string, output string) { + log.Printf("==> Output Path: %s", output) + + archives := getArchives(paths) + for i, f := range archives { + log.Printf("==> Extracting Archive (%d/%d): %s", i, len(archives), f) + + start := time.Now() + + size, files, _, err := xtractr.ExtractFile(&xtractr.XFile{ + FilePath: f, // Path to archive being extracted. + OutputDir: output, // Folder to extract archive into. + FileMode: 0644, // nolint:gomnd // Write files with this mode. + DirMode: 0755, // nolint:gomnd // Write folders with this mode. + Password: "", // (RAR) Archive password. Blank for none. + }) + if err != nil { + log.Printf("[ERROR] Archive: %s: %v", f, err) + continue + } + + elapsed := time.Since(start).Round(time.Millisecond) + log.Printf("==> Extracted Archive %s in %v: bytes: %d, files: %d", f, elapsed, size, len(files)) + log.Printf("==> Files:\n - %s", strings.Join(files, "\n - ")) + } +} + +func getArchives(paths []string) []string { + archives := []string{} + + for _, f := range paths { + switch fileInfo, err := os.Stat(f); { + case err != nil: + log.Fatalf("[ERROR] Reading Path: %s: %s", f, err) + case fileInfo.IsDir(): + archives = append(archives, xtractr.FindCompressedFiles(f)...) + default: + archives = append(archives, f) + } + } + + return archives +} diff --git a/files.go b/files.go index 419979b..7a8bf57 100644 --- a/files.go +++ b/files.go @@ -106,7 +106,7 @@ func getCompressedFiles(hasrar bool, path string, fileList []os.FileInfo) []stri files = append(files, FindCompressedFiles(filepath.Join(path, file.Name()))...) case strings.HasSuffix(lowerName, ".zip") || strings.HasSuffix(lowerName, ".tar") || strings.HasSuffix(lowerName, ".tgz") || strings.HasSuffix(lowerName, ".gz") || - strings.HasSuffix(lowerName, ".bz2"): + strings.HasSuffix(lowerName, ".bz2") || strings.HasSuffix(lowerName, ".7z"): files = append(files, filepath.Join(path, file.Name())) case strings.HasSuffix(lowerName, ".rar"): // Some archives are named poorly. Only return part01 or part001, not all. @@ -146,6 +146,8 @@ func ExtractFile(x *XFile) (int64, []string, []string, error) { //nolint:cyclop switch s := strings.ToLower(x.FilePath); { case strings.HasSuffix(s, ".rar"), strings.HasSuffix(s, ".r00"): return ExtractRAR(x) + case strings.HasSuffix(s, ".7z"): + size, files, err = Extract7z(x) case strings.HasSuffix(s, ".zip"): size, files, err = ExtractZIP(x) case strings.HasSuffix(s, ".tar.gz") || strings.HasSuffix(s, ".tgz"): diff --git a/go.mod b/go.mod index 184ab46..5bb5920 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module golift.io/xtractr go 1.16 -require github.com/nwaples/rardecode v1.1.0 +require ( + github.com/nwaples/rardecode v1.1.0 + github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda // indirect + github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f // indirect + github.com/ulikunitz/xz v0.5.10 // indirect +) diff --git a/go.sum b/go.sum index 586aa2f..0cabc46 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,8 @@ github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda h1:h+YpzUB/bGVJcLqW+d5GghcCmE/A25KbzjXvWJQi/+o= +github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda/go.mod h1:MSotTrCv1PwoR8QgU1JurEx+lNNbtr25I+m0zbLyAGw= +github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f h1:1cJITU3JUI8qNS5T0BlXwANsVdyoJQHQ4hvOxbunPCw= +github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f/go.mod h1:LyBTue+RWeyIfN3ZJ4wVxvDuvlGJtDgCLgCb6HCPgps= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/rar.go b/rar.go index 15bf895..8f930c0 100644 --- a/rar.go +++ b/rar.go @@ -13,7 +13,7 @@ import ( "github.com/nwaples/rardecode" ) -// ExtractRAR extracts a rar file.. to a destination. Simple enough. +// ExtractRAR extracts a rar file. to a destination. This wraps github.com/nwaples/rardecode. func ExtractRAR(x *XFile) (int64, []string, []string, error) { rarReader, err := rardecode.OpenReader(x.FilePath, x.Password) if err != nil {