diff --git a/MANUAL.md b/MANUAL.md index ff3e657..8128c59 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -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 --- @@ -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. @@ -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 diff --git a/go.mod b/go.mod index 0a19476..bf6b041 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/go.sum b/go.sum index 8d13a28..91aaf9f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index a38b79d..d6540e0 100644 --- a/main.go +++ b/main.go @@ -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.") diff --git a/pkg/xt/job.go b/pkg/xt/job.go index 5d9e5e0..bbc3069 100644 --- a/pkg/xt/job.go +++ b/pkg/xt/job.go @@ -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. @@ -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. diff --git a/pkg/xt/xt.go b/pkg/xt/xt.go index c0de314..bccd3fa 100644 --- a/pkg/xt/xt.go +++ b/pkg/xt/xt.go @@ -2,6 +2,7 @@ package xt import ( + "fmt" "log" "os" "path/filepath" @@ -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 { @@ -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...) } }