Module: Omnizip::Convenience

Included in:
Omnizip
Defined in:
lib/omnizip/convenience.rb

Overview

Convenience methods for common archive operations.

All public methods accept an optional format: keyword (default :zip) that selects the underlying archive handler via Omnizip::ArchiveHandler. New formats gain access to the convenience API by registering a handler; no edits to this file are needed.

Constant Summary collapse

DEFAULT_FORMAT =
:zip
ARCHIVE_FORMAT_EXTENSIONS =

Extension -> single-file compressor map. Each lambda takes (input_path, output_path, options). These formats are streams, not archives, so they bypass ArchiveHandler. Extensions naming archive formats with a registered handler; routed when the caller did not pass an explicit format:.

{
  ".zip" => :zip,
  ".tar" => :tar,
  ".7z" => :seven_zip,
}.freeze
READ_ONLY_FORMAT_EXTENSIONS =

Extensions naming real formats this gem can READ but not write through the convenience API. Writing them a ZIP under a foreign name was silent corruption; failing truthfully is the only honest behavior.

[".rar", ".iso", ".cpio"].freeze
READ_ARCHIVE_FORMAT_EXTENSIONS =

Extensions whose format has a READ-ONLY handler: extraction and listing route to it, while creation keeps raising.

{
  ".rar" => :rar,
}.freeze
DECOMPRESSORS =

Extension -> single-file decompressor (stream interface). The Gzip/Bzip2File/Xz classes take path or stream; use the ones exposing decompress_stream for symmetry. Xz exposes a path-based decompress rather than a stream one, so it is marked specially here.

{
  ".gz" => Formats::Gzip,
  ".bz2" => Formats::Bzip2File,
  ".xz" => :xz,
  ".lzma" => Formats::LzmaAlone,
  ".lz" => Formats::Lzip,
  ".zst" => :zstandard,
}.freeze
SINGLE_FILE_COMPRESSORS =
{
  ".gz" => lambda do |input, output, options|
    Omnizip::Formats::Gzip.compress(input, output, options)
  end,
  ".bz2" => lambda do |input, output, options|
    Omnizip::Formats::Bzip2File.compress(input, output, options)
  end,
  ".xz" => lambda do |input, output, options|
    Omnizip::Formats::Xz.create(::File.binread(input), output, options)
  end,
  ".lzma" => lambda do |input, output, options|
    ::File.open(input, "rb") do |input_io|
      ::File.open(output, "wb") do |output_io|
        Omnizip::Formats::LzmaAlone.compress_stream(input_io, output_io,
                                                    options)
      end
    end
  end,
  ".lz" => lambda do |input, output, options|
    ::File.open(input, "rb") do |input_io|
      ::File.open(output, "wb") do |output_io|
        Omnizip::Formats::Lzip.compress_stream(input_io, output_io,
                                               options)
      end
    end
  end,
  ".zst" => lambda do |input, output, options|
    ::File.open(input, "rb") do |input_io|
      ::File.open(output, "wb") do |output_io|
        Omnizip::Algorithms::Zstandard.new.compress(input_io, output_io,
                                                    options)
      end
    end
  end,
}.freeze

Instance Method Summary collapse

Instance Method Details

#add_to_archive(archive_path, entry_name, source_path, format: DEFAULT_FORMAT) ⇒ String

Add a file to an existing archive.

Parameters:

  • archive_path (String)

    Path to archive

  • entry_name (String)

    Entry name in archive

  • source_path (String)

    Path to source file

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

Returns:

  • (String)

    archive_path



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/omnizip/convenience.rb', line 175

def add_to_archive(archive_path, entry_name, source_path,
                   format: DEFAULT_FORMAT)
  require_archive!(archive_path)
  unless ::File.exist?(source_path)
    raise Errno::ENOENT, "Source file not found: #{source_path}"
  end

  resolved = resolve_archive_format(archive_path, format)
  handler = Omnizip::ArchiveHandler.for(resolved)
  # allowed: handler is a registered duck; missing add_entry raises below
  if handler.respond_to?(:add_entry)
    handler.add_entry(archive_path, entry_name, source_path)
  else
    raise Omnizip::UnsupportedFormatError,
          "Format #{resolved.inspect} does not support adding entries"
  end

  archive_path
end

#compress_directory(input_dir, output_path, format: DEFAULT_FORMAT, recursive: true, **options) ⇒ String

Compress a directory into an archive.

Parameters:

  • input_dir (String)

    Path to input directory

  • output_path (String)

    Path to output archive

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

  • recursive (Boolean) (defaults to: true)

    Include subdirectories (default true)

  • options (Hash)

    Compression / format-specific options

Returns:

  • (String)

    output_path



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/omnizip/convenience.rb', line 95

def compress_directory(input_dir, output_path, format: DEFAULT_FORMAT,
                       recursive: true, **options)
  unless ::File.exist?(input_dir)
    raise Errno::ENOENT, "Input directory not found: #{input_dir}"
  end
  unless ::File.directory?(input_dir)
    raise ArgumentError, "Input is not a directory: #{input_dir}"
  end

  # A directory cannot be written through a single-file stream
  # format; routing it to the default would silently produce a
  # ZIP under a foreign extension.
  if format == DEFAULT_FORMAT && single_file_handler(output_path)
    raise Omnizip::UnsupportedFormatError,
          "#{::File.extname(output_path).downcase} is a single-file " \
          "stream format and cannot contain a directory; use an " \
          "archive format (.zip/.tar/.7z) or compress entries " \
          "individually"
  end

  if options[:profile]
    first_file = find_first_file(input_dir)
    apply_profile(first_file, options)
  end

  Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
    add_directory_contents(archive, input_dir, "", recursive: recursive)
  end

  output_path
end

#compress_file(input_path, output_path, format: DEFAULT_FORMAT, **options) ⇒ String

Compress a single file into an archive.

Parameters:

  • input_path (String)

    Path to input file

  • output_path (String)

    Path to output archive

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

  • options (Hash)

    Compression / format-specific options

Returns:

  • (String)

    output_path



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/omnizip/convenience.rb', line 20

def compress_file(input_path, output_path, format: DEFAULT_FORMAT, **options)
  unless ::File.exist?(input_path)
    raise Errno::ENOENT, "Input file not found: #{input_path}"
  end
  if ::File.directory?(input_path)
    raise ArgumentError, "Input is a directory: #{input_path}"
  end

  options = apply_profile(input_path, options) if options[:profile]

  if options[:chunked]
    return Omnizip::Chunked.compress_file(input_path, output_path, **options)
  end

  # Single-file compression formats route by output extension
  # (unless an archive format was requested explicitly): writing
  # output.lzma through the archive path silently produced a ZIP
  # under a foreign extension.
  if format == DEFAULT_FORMAT && (handler = single_file_handler(output_path))
    handler.call(input_path, output_path, options)
    return output_path
  end

  Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
    archive.add(::File.basename(input_path), input_path)
  end

  output_path
end

#create_rar(archive_path, **options, &block) ⇒ Object

Create a RAR archive (requires RAR license — see NotLicensedError). rubocop:disable-next Naming/BlockForwarding, Style/ArgumentsForwarding -- Ruby 3.0 compatibility



217
218
219
220
# File 'lib/omnizip/convenience.rb', line 217

def create_rar(archive_path, **options, &block)
  options[:version] ||= 5
  Omnizip::Formats::Rar.create(archive_path, options, &block)
end

#decompress_file(compressed_path, output_path) ⇒ String

Decompress a single-file compressed stream (the counterpart of #compress_file). The format is resolved from the input extension: .gz, .bz2, .xz, .lzma, .lz.

Parameters:

  • compressed_path (String)

    Path to the compressed file

  • output_path (String)

    Path to write the decompressed data

Returns:

  • (String)

    output_path



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/omnizip/convenience.rb', line 57

def decompress_file(compressed_path, output_path)
  require_archive!(compressed_path)

  format_class = DECOMPRESSORS[::File.extname(compressed_path).downcase]
  unless format_class
    raise Omnizip::UnsupportedFormatError,
          "decompress_file supports #{DECOMPRESSORS.keys.join(', ')}; " \
          "use extract_archive for archive formats"
  end

  case format_class
  when :xz
    ::File.binwrite(output_path, Formats::Xz.decompress(compressed_path))
  when :zstandard
    zstd = Algorithms::Zstandard.new
    ::File.open(compressed_path, "rb") do |input_io|
      ::File.open(output_path, "wb") do |output_io|
        zstd.decompress(input_io, output_io)
      end
    end
  else
    ::File.open(compressed_path, "rb") do |input_io|
      ::File.open(output_path, "wb") do |output_io|
        format_class.decompress_stream(input_io, output_io)
      end
    end
  end
  output_path
end

#extract_archive(archive_path, output_dir, format: DEFAULT_FORMAT, overwrite: false, **options) ⇒ Array<String>

Extract an archive to a directory.

Parameters:

  • archive_path (String)

    Path to archive

  • output_dir (String)

    Output directory

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

  • overwrite (Boolean) (defaults to: false)

    Overwrite existing files (default false)

  • options (Hash)

    Format-specific extraction options

Returns:

  • (Array<String>)

    Extracted file paths



135
136
137
138
139
140
141
# File 'lib/omnizip/convenience.rb', line 135

def extract_archive(archive_path, output_dir, format: DEFAULT_FORMAT,
                    overwrite: false, **options)
  require_archive!(archive_path)
  Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
    .extract_to(archive_path, output_dir,
                overwrite: overwrite, **options)
end

#list_archive(archive_path, format: DEFAULT_FORMAT, details: false, **options) ⇒ Array<String>, Array<Hash>

List contents of an archive.

Parameters:

  • archive_path (String)

    Path to archive

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

  • details (Boolean) (defaults to: false)

    Include detailed info (default false)

  • options (Hash)

    Format-specific listing options

Returns:

  • (Array<String>, Array<Hash>)


150
151
152
153
154
155
# File 'lib/omnizip/convenience.rb', line 150

def list_archive(archive_path, format: DEFAULT_FORMAT, details: false,
                 **options)
  require_archive!(archive_path)
  Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
    .list(archive_path, details: details, **options)
end

#read_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT) ⇒ String

Read a single entry from an archive.

Parameters:

  • archive_path (String)

    Path to archive

  • entry_name (String)

    Entry to read

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

Returns:

  • (String)

    Entry contents



163
164
165
166
# File 'lib/omnizip/convenience.rb', line 163

def read_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
  require_archive!(archive_path)
  Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false)).read_entry(archive_path, entry_name)
end

#remove_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT) ⇒ String

Remove an entry from an existing archive.

Parameters:

  • archive_path (String)

    Path to archive

  • entry_name (String)

    Entry to remove

  • format (Symbol) (defaults to: DEFAULT_FORMAT)

    Archive format (default :zip)

Returns:

  • (String)

    archive_path



201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/omnizip/convenience.rb', line 201

def remove_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
  require_archive!(archive_path)
  resolved = resolve_archive_format(archive_path, format)
  handler = Omnizip::ArchiveHandler.for(resolved)
  # allowed: handler is a registered duck; missing remove_entry raises below
  unless handler.respond_to?(:remove_entry)
    raise Omnizip::UnsupportedFormatError,
          "Format #{resolved.inspect} does not support removing entries"
  end

  handler.remove_entry(archive_path, entry_name)
  archive_path
end

#resolve_archive_format(path, format, writing: true) ⇒ Object

Archive format for a path: an explicit format wins unless it is the default; otherwise the extension routes. Known-but- unwritable extensions raise instead of receiving a mislabeled ZIP. writing: false (read operations) additionally routes extensions with read-only handlers.



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/omnizip/convenience.rb', line 306

def resolve_archive_format(path, format, writing: true)
  return format unless format == DEFAULT_FORMAT

  ext = ::File.extname(path).downcase
  routed = ARCHIVE_FORMAT_EXTENSIONS[ext]
  routed ||= READ_ARCHIVE_FORMAT_EXTENSIONS[ext] unless writing
  return routed if routed

  if READ_ONLY_FORMAT_EXTENSIONS.include?(ext)
    raise Omnizip::UnsupportedFormatError,
          "#{ext} archives cannot be written by the convenience " \
          "API (read-only support); use the format-specific reader"
  end

  DEFAULT_FORMAT
end

#single_file_handler(output_path) ⇒ Object

The single-file compressor matching the output extension, or nil.



296
297
298
299
# File 'lib/omnizip/convenience.rb', line 296

def single_file_handler(output_path)
  ext = ::File.extname(output_path).downcase
  SINGLE_FILE_COMPRESSORS[ext]
end