Basic Usage

Installation

Add to your Gemfile:

gem "excavate"

Or install directly:

gem install excavate

Basic Extraction

The simplest way to extract an archive:

require "excavate"

# Extract to auto-created directory
Excavate::Archive.new("package.zip").extract
# Creates ./package/ directory with contents

# Extract to specific directory
Excavate::Archive.new("package.zip").extract("/path/to/target")

Automatic Target Directory

When no target is specified, Excavate creates a directory with the archive’s base name (without extension) in the current working directory.

Target Directory Rules

The target directory must be empty. If it exists and contains files, a TargetNotEmptyError is raised. This prevents accidental overwriting.

# This will raise TargetNotEmptyError if ./package/ exists and is not empty
Excavate::Archive.new("package.zip").extract("./package")

Recursive Extraction

Extract nested archives automatically. This is particularly useful for:

  • MSI installers containing CAB files

  • Self-extracting executables

  • Complex software distributions

  • macOS installer packages (PKG)

# Extract MSI and all nested CAB files
Excavate::Archive.new("installer.msi").extract(recursive_packages: true)

# Result: All files from MSI and nested CABs in target directory

How Recursive Extraction Works

  1. Extract the top-level archive

  2. Scan extracted files for nested archives

  3. Extract each nested archive

  4. Replace archive with its contents

  5. Repeat until no more nested archives found

Example: MSI Installation

# MSI files contain nested CAB archives
Excavate::Archive.new("fonts.msi").extract(recursive_packages: true)

# Before: fonts.msi contains data1.cab, data2.cab
# After: Target directory contains all font files directly

Example: macOS PKG

# PKG files are XAR archives containing CPIO payloads
Excavate::Archive.new("Installer.pkg").extract(recursive_packages: true)

# Automatically extracts XAR, then CPIO, then GZIP

Selective Extraction

Extract only specific files from an archive:

# Extract specific files by path
Excavate::Archive.new("data.tar.gz").extract(
  files: ["config/settings.json", "data/users.csv"]
)

# Extract from nested archives
Excavate::Archive.new("package.msi").extract(
  files: ["fonts.zip/Arial.ttf"],
  recursive_packages: true
)

If a specified file is not found, a TargetNotFoundError is raised.

Nested Archive Paths

When extracting from nested archives, use the format archive/path/to/file:

# Extract file from CAB inside MSI
Excavate::Archive.new("setup.msi").extract(
  files: ["data1.cab/config.ini"],
  recursive_packages: true
)

Filter-Based Extraction

Extract files matching a glob pattern:

# Extract all TrueType fonts
Excavate::Archive.new("fonts.zip").extract(filter: "**/*.ttf")

# Extract configuration files
Excavate::Archive.new("backup.tar").extract(filter: "etc/**/*.conf")

# Extract multiple extensions
Excavate::Archive.new("assets.zip").extract(filter: "**/*.{png,jpg,svg}")

Glob Pattern Syntax

The filter supports standard glob patterns:

Pattern Description

*

Matches any characters except /

**

Matches any directories recursively

?

Matches any single character

[abc]

Matches any character in brackets

{a,b}

Matches either pattern

Pattern Examples

# All JavaScript files
filter: "**/*.js"

# Files in specific directory
filter: "src/**/*.rb"

# Multiple file types
filter: "**/*.{html,css,js}"

# Specific filename pattern
filter: "**/test_*.rb"

File Iteration

Use the files method to process extracted files without managing directories:

# Collect all font files
fonts = Excavate::Archive.new("package.zip").files(
  recursive_packages: true,
  filter: "**/*.ttf"
) do |file_path|
  puts "Found: #{file_path}"
  file_path
end

puts "Total fonts: #{fonts.size}"

The files method:

  1. Creates a temporary directory

  2. Extracts files based on parameters

  3. Yields each file path to your block

  4. Cleans up the temporary directory

  5. Returns an array of block results

Processing During Extraction

# Analyze files during extraction
result = Excavate::Archive.new("logs.tar.gz").files do |path|
  {
    path: path,
    size: File.size(path),
    ext: File.extname(path)
  }
end

# result is an array of hashes

Working with Different Formats

Windows Installers (MSI)

MSI files are OLE compound documents containing CAB archives:

# Extract MSI and all embedded CABs
Excavate::Archive.new("setup.msi").extract(recursive_packages: true)

# MSI structure:
# setup.msi
# ├── data1.cab (extracted automatically)
# ├── data2.cab (extracted automatically)
# └── final files

Self-Extracting Archives

EXE files may be 7-Zip or CAB self-extracting archives:

# Automatically detects format
Excavate::Archive.new("installer.exe").extract(recursive_packages: true)

# Falls back from 7z to CAB if needed

RPM Packages

RPM files contain compressed CPIO payloads:

# Extract RPM package
Excavate::Archive.new("package.rpm").extract(recursive_packages: true)

# Extracts payload.cpio.gz automatically

Compressed Tarballs

Common Unix formats are fully supported:

Excavate::Archive.new("package.tar.gz").extract(recursive_packages: true)
Excavate::Archive.new("package.tar.xz").extract(recursive_packages: true)
Excavate::Archive.new("package.tar.bz2").extract(recursive_packages: true)

macOS Packages

XAR-based packages (.pkg):

Excavate::Archive.new("Installer.pkg").extract(recursive_packages: true)

# PKG structure:
# Installer.pkg
# ├── Distribution (XML)
# └── Package.pkg (nested archive)
#     └── Payload (CPIO.GZ)

Command-Line Interface

Excavate includes a CLI for quick extraction tasks:

# Basic extraction
excavate package.zip

# Recursive extraction
excavate --recursive installer.msi

# Extract specific files
excavate data.tar.gz config/settings.json

# Filter by pattern
excavate --filter "**/*.ttf" fonts.zip

# Combine options
excavate --recursive --filter "**/*.conf" package.msi

See CLI Reference for complete documentation.

Error Handling

Excavate provides specific error classes:

require "excavate"

begin
  Excavate::Archive.new("archive.zip").extract("/existing/dir")
rescue Excavate::TargetNotEmptyError => e
  puts "Directory not empty: #{e.message}"
rescue Excavate::TargetNotFoundError => e
  puts "File not found: #{e.message}"
rescue Excavate::UnknownArchiveError => e
  puts "Unsupported format: #{e.message}"
rescue Excavate::Error => e
  puts "Extraction failed: #{e.message}"
end

Error Classes

Error Description

TargetExistsError

Target file or directory already exists

TargetNotEmptyError

Target directory is not empty

TargetNotFoundError

Requested file or pattern not found in archive

UnknownArchiveError

Archive format not recognized

Rails Integration

Excavate works with Rails uploaded files:

class UploadsController < ApplicationController
  def upload
    archive = Excavate::Archive.new(params[:file])

    # Extract to temporary directory
    target = Dir.mktmpdir
    archive.extract(target, recursive_packages: true)

    # Process extracted files
    # ...
  ensure
    FileUtils.rm_rf(target)
  end
end

The archive parameter accepts any object responding to original_filename.