Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ DESCRIPTION

* This application recursively extracts compressed archives.
* Provide directories to search or provide files to extract.
* Supports ZIP, RAR, GZIP, BZIP2, TAR, TGZ, TBZ2, 7ZIP, ISO9660
* ie. *.zip *.rar *.r00 *.gz *.bz2 *.tar *.tgz *.tbz2 *.7z *.iso
* Supports: ZIP, RAR, GZIP, BZIP2, TAR, TGZ, TBZ2, 7ZIP, ISO9660
* Supports: Z, AR, BR, CPIO, DEB, LZ/4, LZIP, LZMA2, S2, SNAPPY
* Supports: RPM, SZ, TLZ, TXZ, ZLIB, ZSTD, BROTLI, ZZ
* ie: *.zip *.rar *.r00 *.gz *.bz2 *.tar *.tgz *.tbz2 *.7z *.iso (and others)

OPTIONS
---
Expand Down Expand Up @@ -58,6 +60,9 @@ OPTIONS
This option determines if the archives will be extracted to their
parent folder. Using this flag will override the --output option.

-V, --verbose
Verbose logging prints the extracted file paths.

-D, --debug
Enable debug output.

Expand All @@ -83,6 +88,8 @@ Example TOML job file:
min_depth = 1
file_mode = 644
dir_mode = 755
verbose = false
debug = false
preserve_paths = false

AUTHOR
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ toolchain go1.24.2
require (
github.com/spf13/pflag v1.0.6
golift.io/version v0.0.2
golift.io/xtractr v0.2.3-0.20250419070747-b391d40d7453
golift.io/xtractr v0.2.3-0.20250419170021-53bfe05970fe
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ golift.io/cnfgfile v0.0.0-20240713024420-a5436d84eb48 h1:c7cJWRr0cUnFHKtq072esKz
golift.io/cnfgfile v0.0.0-20240713024420-a5436d84eb48/go.mod h1:zHm9o8SkZ6Mm5DfGahsrEJPsogyR0qItP59s5lJ98/I=
golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE=
golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU=
golift.io/xtractr v0.2.3-0.20250419070747-b391d40d7453 h1:JnozjdGe8GNAgcLDxd6WjWxVpktsmqJzP2PMT+DulhE=
golift.io/xtractr v0.2.3-0.20250419070747-b391d40d7453/go.mod h1:invEOYfyBnFtegY2V2n+9K5bUEHB8pGZng1BK0U2r38=
golift.io/xtractr v0.2.3-0.20250419170021-53bfe05970fe h1:fCqAf/BYLNy5BdX6IeeGIGutHqOANjIfKeYANd5Cktg=
golift.io/xtractr v0.2.3-0.20250419170021-53bfe05970fe/go.mod h1:invEOYfyBnFtegY2V2n+9K5bUEHB8pGZng1BK0U2r38=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func parseFlags(pwd string) (*xt.Job, *flags) {
flag.StringSliceVarP(&job.Passwords, "password", "P", nil, "Attempt these passwords for rar and 7zip archives.")
flag.BoolVarP(&job.SquashRoot, "squash-root", "S", false,
"If archive contains only 1 folder at in the root, its contents are moved into output folder.")
flag.BoolVarP(&job.Verbose, "verbose", "V", false, "Verbose output prints the file list that was extracted.")
flag.BoolVarP(&job.DebugLog, "debug", "D", false, "Enable debug output.")
flag.StringSliceVarP(&flags.JobFiles, "job-file", "j", nil, "Read additional extraction jobs from these files.")
flag.BoolVarP(&job.Preserve, "preserve-paths", "p", false, "Recreate directory hierarchy while extracting.")
Expand Down
5 changes: 3 additions & 2 deletions pkg/xt/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Job struct {
SquashRoot bool `json:"squashRoot" toml:"squash_root" xml:"squash_root" yaml:"squashRoot"`
DebugLog bool `json:"debugLog" toml:"debug_log" xml:"debug_log" yaml:"debugLog"`
Preserve bool `json:"preservePaths" toml:"preserve_paths" xml:"preserve_paths" yaml:"preservePaths"`
Verbose bool `json:"verbose" toml:"verbose" xml:"verbose" yaml:"verbose"`
}

// ParseJobs checks for and reads more jobs in from 0 or more job files.
Expand Down Expand Up @@ -48,8 +49,8 @@ func (j *Job) String() string {
sSfx = "s"
}

return fmt.Sprintf("%d path%s, f/d-mode:%s/%s, min/max-depth: %d/%d output: %s",
len(j.Paths), sSfx, j.FileMode, j.DirMode, j.MinDepth, j.MaxDepth, j.Output)
return fmt.Sprintf("%d path%s, f/d-mode:%s/%s, min/max-depth: %d/%d, preserve/squash: %v/%v output: %s",
len(j.Paths), sSfx, j.FileMode, j.DirMode, j.MinDepth, j.MaxDepth, j.Preserve, j.SquashRoot, j.Output)
}

// Debugf prints a log message if debug is enabled.
Expand Down
91 changes: 54 additions & 37 deletions pkg/xt/xt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package xt

import (
"fmt"
"log"
"os"
"path/filepath"
Expand All @@ -20,54 +21,69 @@ func Extract(job *Job) {

job.fixModes()

total := 0
total := archives.Count()
count := 0
size := int64(0)
fCount := 0
start := time.Now()

for _, files := range archives {
total += len(files)
}

for _, files := range archives {
for _, fileName := range files {
for folder, files := range archives {
for _, archive := range files {
count++
log.Printf("==> Extracting Archive (%d/%d): %s", count, total, fileName)

file := &xtractr.XFile{
FilePath: fileName, // Path to archive being extracted.
OutputDir: job.Output, // Folder to extract archive into.
FileMode: job.FileMode.Mode(), // Write files with this mode.
DirMode: job.DirMode.Mode(), // Write folders with this mode.
Passwords: job.Passwords, // (RAR/7zip) Archive password(s).
SquashRoot: job.SquashRoot, // Remove single root folder?
}

file.SetLogger(job)

start := time.Now()
log.Printf("==> Extracting Archive (%d/%d): %s", count, total, archive)

// If preserving the file hierarchy, set the output directory to the
// folder of the archive being extracted.
if job.Preserve {
file.OutputDir = filepath.Dir(fileName)
}

size, files, _, err := xtractr.ExtractFile(file)
output, fSize, files, duration, err := job.processArchive(folder, archive)
if err != nil {
log.Printf("[ERROR] Archive: %s: %v", fileName, err)
continue
log.Printf("[ERROR] Extracting: %v", err)
} else {
log.Printf("==> Extracted Archive %s to %s in %v: bytes: %d, files: %d",
archive, output, duration.Round(time.Millisecond), fSize, len(files))
}

log.Printf("==> Extracted Archive %s in %v: bytes: %d, files: %d",
fileName, time.Since(start).Round(time.Millisecond), size, len(files))

if len(files) > 0 {
if len(files) > 0 && job.Verbose {
log.Printf("==> Files:\n - %s", strings.Join(files, "\n - "))
}

size += fSize
fCount += len(files)
}
}

log.Printf("==> Done.")
log.Printf("==> Extracted %d archives; wrote %d bytes into %d files in %v",
total, size, fCount, time.Since(start).Round(time.Millisecond))
}

func (j *Job) processArchive(folder, archive string) (string, int64, []string, time.Duration, error) {
file := &xtractr.XFile{
FilePath: archive, // Path to archive being extracted.
OutputDir: j.Output, // Folder to extract archive into.
FileMode: j.FileMode.Mode(), // Write files with this mode.
DirMode: j.DirMode.Mode(), // Write folders with this mode.
Passwords: j.Passwords, // (RAR/7zip) Archive password(s).
SquashRoot: j.SquashRoot, // Remove single root folder?
}
file.SetLogger(j)

// If preserving the file hierarchy: set the output directory to the same path as the input file.
if j.Preserve {
// Remove input path prefix from fileName,
// append fileName.Dir to job.Output,
// extract file into job.Output/file(sub)Folder(s).
file.OutputDir = filepath.Join(j.Output, filepath.Dir(strings.TrimPrefix(archive, folder)))
}

start := time.Now()

size, files, _, err := xtractr.ExtractFile(file)
if err != nil {
err = fmt.Errorf("archive: %s: %w", archive, err)
}

return file.OutputDir, size, files, time.Since(start), err
}

func (j *Job) getArchives() map[string][]string {
func (j *Job) getArchives() xtractr.ArchiveList {
archives := map[string][]string{}

for _, fileName := range j.Paths {
Expand All @@ -87,13 +103,14 @@ func (j *Job) getArchives() map[string][]string {
exclude = xtractr.AllExcept(j.Include...)
}

for folder, fileList := range xtractr.FindCompressedFiles(xtractr.Filter{
for _, fileList := range xtractr.FindCompressedFiles(xtractr.Filter{
Path: fileName,
ExcludeSuffix: exclude,
MaxDepth: int(j.MaxDepth),
MinDepth: int(j.MinDepth),
}) {
archives[folder] = fileList
// Group archive lists by the parent search folder that found them.
archives[fileName] = append(archives[fileName], fileList...)
}
}

Expand Down