Class: Archive::Zip

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/archive/zip.rb,
lib/archive/zip/codec.rb,
lib/archive/zip/entry.rb,
lib/archive/zip/entry.rb,
lib/archive/zip/entry.rb,
lib/archive/zip/entry.rb,
lib/archive/zip/error.rb,
lib/archive/zip/version.rb,
lib/archive/zip/codec/store.rb,
lib/archive/zip/extra_field.rb,
lib/archive/zip/codec/deflate.rb,
lib/archive/zip/data_descriptor.rb,
lib/archive/zip/extra_field/raw.rb,
lib/archive/zip/extra_field/unix.rb,
lib/archive/zip/codec/null_encryption.rb,
lib/archive/zip/codec/traditional_encryption.rb,
lib/archive/zip/extra_field/extended_timestamp.rb

Overview

Archive::Zip represents a ZIP archive compatible with InfoZip tools and the archives they generate. It currently supports both stored and deflated ZIP entries, directory entries, file entries, and symlink entries. File and directory accessed and modified times, POSIX permissions, and ownerships can be archived and restored as well depending on platform support for such metadata. Traditional (weak) encryption is also supported.

Zip64, digital signatures, and strong encryption are not supported. ZIP archives can only be read from seekable kinds of IO, such as files; reading archives from pipes or any other non-seekable kind of IO is not supported. However, writing to such IO objects IS supported.

Defined Under Namespace

Modules: Codec, Entry, ExtraField Classes: DataDescriptor, EntryError, Error, ExtraFieldError, IOError, UnzipError

Constant Summary collapse

EOCD_SIGNATURE =

The lead-in marker for the end of central directory record.

"PK\x5\x6"
DS_SIGNATURE =

The lead-in marker for the digital signature record.

"PK\x5\x5"
Z64EOCD_SIGNATURE =

The lead-in marker for the ZIP64 end of central directory record.

"PK\x6\x6"
Z64EOCDL_SIGNATURE =

The lead-in marker for the ZIP64 end of central directory locator record.

"PK\x6\x7"
CFH_SIGNATURE =

The lead-in marker for a central file record.

"PK\x1\x2"
LFH_SIGNATURE =

The lead-in marker for a local file record.

"PK\x3\x4"
DD_SIGNATURE =

The lead-in marker for data descriptor record.

"PK\x7\x8"
VERSION =

The current version of this gem.

"0.4.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(archive, mode = :r) ⇒ Zip

Opens an existing archive and/or creates a new archive.

If archive is a String, it will be treated as a file path; otherwise, it is assumed to be an IO-like object with the necessary read or write support depending on the setting of mode. IO-like objects are not closed when the archive is closed, but files opened from file paths are. Set mode to :r or "r" to read the archive, and set it to :w or "w" to write the archive.

NOTE: The #close method must be called in order to save any modifications to the archive. Due to limitations in the Ruby finalization capabilities, the #close method is not automatically called when this object is garbage collected. Make sure to call #close when finished with this object.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/archive/zip.rb', line 133

def initialize(archive, mode = :r)
  @archive = archive
  mode = mode.to_sym
  if mode == :r || mode == :w then
    @mode = mode
  else
    raise ArgumentError, "illegal access mode #{mode}"
  end

  @close_delegate = false
  if @archive.kind_of?(String) then
    @close_delegate = true
    if mode == :r then
      @archive = File.open(@archive, 'rb')
    else
      @archive = File.open(@archive, 'wb')
    end
  end
  @entries = []
  @comment = ''
  @closed = false
end

Instance Attribute Details

#commentObject

A comment string for the ZIP archive.



157
158
159
# File 'lib/archive/zip.rb', line 157

def comment
  @comment
end

Class Method Details

.archive(archive, paths, options = {}) ⇒ Object

Creates or possibly updates an archive using paths for new contents.

If archive is a String, it is treated as a file path which will receive the archive contents. If the file already exists, it is assumed to be an archive and will be updated "in place". Otherwise, a new archive is created. The archive will be closed once written.

If archive has any other kind of value, it is treated as a writable IO-like object which will be left open after the completion of this method.

NOTE: No attempt is made to prevent adding multiple entries with the same archive path.

See the instance method #archive for more information about paths and options.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/archive/zip.rb', line 62

def self.archive(archive, paths, options = {})
  if archive.kind_of?(String) && File.exist?(archive) then
    # Update the archive "in place".
    tmp_archive_path = nil
    File.open(archive) do |archive_in|
      Tempfile.open(*File.split(archive_in.path).reverse) do |archive_out|
        # Save off the path so that the temporary file can be renamed to the
        # archive file later.
        tmp_archive_path = archive_out.path
        # Ensure the file is in binary mode for Windows.
        archive_out.binmode
        # Update the archive.
        open(archive_in, :r) do |z_in|
          open(archive_out, :w) do |z_out|
            z_in.each  { |entry| z_out << entry }
            z_out.archive(paths, options)
          end
        end
      end
    end
    # Set more reasonable permissions than those set by Tempfile.
    File.chmod(0666 & ~File.umask, tmp_archive_path)
    # Replace the input archive with the output archive.
    File.rename(tmp_archive_path, archive)
  else
    open(archive, :w) { |z| z.archive(paths, options) }
  end
end

.extract(archive, destination, options = {}) ⇒ Object

Extracts the entries from an archive to destination.

If archive is a String, it is treated as a file path pointing to an existing archive file. Otherwise, it is treated as a seekable and readable IO-like object.

See the instance method #extract for more information about destination and options.



99
100
101
# File 'lib/archive/zip.rb', line 99

def self.extract(archive, destination, options = {})
  open(archive, :r) { |z| z.extract(destination, options) }
end

.open(archive, mode = :r) ⇒ Object

Calls #new with the given arguments and yields the resulting Zip instance to the given block. Returns the result of the block and ensures that the Zip instance is closed.

This is a synonym for #new if no block is given.



108
109
110
111
112
113
114
115
116
117
# File 'lib/archive/zip.rb', line 108

def self.open(archive, mode = :r)
  zf = new(archive, mode)
  return zf unless block_given?

  begin
    yield(zf)
  ensure
    zf.close unless zf.closed?
  end
end

Instance Method Details

#add_entry(entry) ⇒ Object Also known as: <<

Adds entry into a writable ZIP archive.

NOTE: No attempt is made to prevent adding multiple entries with the same archive path.

Raises Archive::Zip::IOError if called on a non-writable archive or after the archive is closed.

Raises:



222
223
224
225
226
227
228
229
230
231
# File 'lib/archive/zip.rb', line 222

def add_entry(entry)
  raise IOError, 'non-writable archive' unless writable?
  raise IOError, 'closed archive' if closed?
  unless entry.kind_of?(Entry) then
    raise ArgumentError, 'Archive::Zip::Entry instance required'
  end

  @entries << entry
  self
end

#archive(paths, options = {}) ⇒ Object

Adds paths to the archive. paths may be either a single path or an Array of paths. The files and directories referenced by paths are added using their respective basenames as their zip paths. The exception to this is when the basename for a path is either "." or "..". In this case, the path is replaced with the paths to the contents of the directory it references.

options is a Hash optionally containing the following: :path_prefix::

Specifies a prefix to be added to the zip_path attribute of each entry
where `/' is the file separator character.  This defaults to the empty
string.  All values are passed through Archive::Zip::Entry.expand_path
before use.

:recursion::

When set to +true+ (the default), the contents of directories are
recursively added to the archive.

:directories::

When set to +true+ (the default), entries are added to the archive for
directories.  Otherwise, the entries for directories will not be added;
however, the contents of the directories will still be considered if the
<b>:recursion</b> option is +true+.

:symlinks::

When set to +false+ (the default), entries for symlinks are excluded
from the archive.  Otherwise, they are included.  <b>NOTE:</b> Unless
<b>:follow_symlinks</b> is explicitly set, it will be set to the logical
NOT of this option in calls to Archive::Zip::Entry.from_file.  If
symlinks should be completely ignored, set both this option and
<b>:follow_symlinks</b> to +false+.  See Archive::Zip::Entry.from_file
for details regarding <b>:follow_symlinks</b>.

:flatten::

When set to +false+ (the default), the directory paths containing
archived files will be included in the zip paths of entries representing
the files.  When set to +true+, files are archived without any
containing directory structure in the zip paths.  Setting to +true+
implies that <b>:directories</b> is +false+ and <b>:path_prefix</b> is
empty.

:exclude::

Specifies a proc or lambda which takes a single argument containing a
prospective zip entry and returns +true+ if the entry should be excluded
from the archive and +false+ if it should be included.  <b>NOTE:</b> If
a directory is excluded in this way, the <b>:recursion</b> option has no
effect for it.

:password::

Specifies a proc, lambda, or a String.  If a proc or lambda is used, it
must take a single argument containing a zip entry and return a String
to be used as an encryption key for the entry.  If a String is used, it
will be used as an encryption key for all encrypted entries.

:on_error::

Specifies a proc or lambda which is called when an exception is raised
during the archival of an entry.  It takes two arguments, a file path
and an exception object generated while attempting to archive the entry.
If <tt>:retry</tt> is returned, archival of the entry is attempted
again.  If <tt>:skip</tt> is returned, the entry is skipped.  Otherwise,
the exception is raised.

Any other options which are supported by Archive::Zip::Entry.from_file are also supported.

NOTE: No attempt is made to prevent adding multiple entries with the same archive path.

Raises Archive::Zip::IOError if called on a non-writable archive or after the archive is closed. Raises Archive::Zip::EntryError if the :on_error option is either unset or indicates that the error should be raised and Archive::Zip::Entry.from_file raises an error.

Example

A directory contains:

zip-test
+- dir1
|  +- file2.txt
+- dir2
+- file1.txt

Create some archives:

Archive::Zip.open('zip-test1.zip') do |z|
z.archive('zip-test')
end

Archive::Zip.open('zip-test2.zip') do |z|
z.archive('zip-test/.', :path_prefix => 'a/b/c/d')
end

Archive::Zip.open('zip-test3.zip') do |z|
z.archive('zip-test', :directories => false)
end

Archive::Zip.open('zip-test4.zip') do |z|
z.archive('zip-test', :exclude => lambda { |e| e.file? })
end

The archives contain:

zip-test1.zip -> zip-test/
               zip-test/dir1/
               zip-test/dir1/file2.txt
               zip-test/dir2/
               zip-test/file1.txt

zip-test2.zip -> a/b/c/d/dir1/
               a/b/c/d/dir1/file2.txt
               a/b/c/d/dir2/
               a/b/c/d/file1.txt

zip-test3.zip -> zip-test/dir1/file2.txt
               zip-test/file1.txt

zip-test4.zip -> zip-test/
               zip-test/dir1/
               zip-test/dir2/

Raises:



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/archive/zip.rb', line 343

def archive(paths, options = {})
  raise IOError, 'non-writable archive' unless writable?
  raise IOError, 'closed archive' if closed?

  # Ensure that paths is an enumerable.
  paths = [paths] unless paths.kind_of?(Enumerable)
  # If the basename of a path is '.' or '..', replace the path with the
  # paths of all the entries contained within the directory referenced by
  # the original path.
  paths = paths.collect do |path|
    basename = File.basename(path)
    if basename == '.' || basename == '..' then
      Dir.entries(path).reject do |e|
        e == '.' || e == '..'
      end.collect do |e|
        File.join(path, e)
      end
    else
      path
    end
  end.flatten.uniq

  # Ensure that unspecified options have default values.
  options[:path_prefix]  = ''    unless options.has_key?(:path_prefix)
  options[:recursion]    = true  unless options.has_key?(:recursion)
  options[:directories]  = true  unless options.has_key?(:directories)
  options[:symlinks]     = false unless options.has_key?(:symlinks)
  options[:flatten]      = false unless options.has_key?(:flatten)

  # Flattening the directory structure implies that directories are skipped
  # and that the path prefix should be ignored.
  if options[:flatten] then
    options[:path_prefix] = ''
    options[:directories] = false
  end

  # Clean up the path prefix.
  options[:path_prefix] = Entry.expand_path(options[:path_prefix].to_s)

  paths.each do |path|
    # Generate the zip path.
    zip_entry_path = File.basename(path)
    zip_entry_path += '/' if File.directory?(path)
    unless options[:path_prefix].empty? then
      zip_entry_path = "#{options[:path_prefix]}/#{zip_entry_path}"
    end

    begin
      # Create the entry, but do not add it to the archive yet.
      zip_entry = Zip::Entry.from_file(
        path,
        options.merge(
          :zip_path        => zip_entry_path,
          :follow_symlinks => options.has_key?(:follow_symlinks) ?
                              options[:follow_symlinks] :
                              ! options[:symlinks]
        )
      )
    rescue StandardError => error
      unless options[:on_error].nil? then
        case options[:on_error][path, error]
        when :retry
          retry
        when :skip
          next
        else
          raise
        end
      else
        raise
      end
    end

    # Skip this entry if so directed.
    if (zip_entry.symlink? && ! options[:symlinks]) ||
       (! options[:exclude].nil? && options[:exclude][zip_entry]) then
      next
    end

    # Set the encryption key for the entry.
    if options[:password].kind_of?(String) then
      zip_entry.password = options[:password]
    elsif ! options[:password].nil? then
      zip_entry.password = options[:password][zip_entry]
    end

    # Add entries for directories (if requested) and files/symlinks.
    if (! zip_entry.directory? || options[:directories]) then
      add_entry(zip_entry)
    end

    # Recurse into subdirectories (if requested).
    if zip_entry.directory? && options[:recursion] then
      archive(
        Dir.entries(path).reject do |e|
          e == '.' || e == '..'
        end.collect do |e|
          File.join(path, e)
        end,
        options.merge(:path_prefix => zip_entry_path)
      )
    end
  end

  nil
end

#closeObject

Closes the archive.

Failure to close the archive by calling this method may result in a loss of data for writable archives.

NOTE: The underlying stream is only closed if the archive was opened with a String for the archive parameter.

Raises Archive::Zip::IOError if called more than once.

Raises:



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/archive/zip.rb', line 168

def close
  raise IOError, 'closed archive' if closed?

  if writable? then
    # Write the new archive contents.
    dump(@archive)
  end

  # Note that we only close delegate streams which are opened by us so that
  # the user may do so for other delegate streams at his/her discretion.
  @archive.close if @close_delegate

  @closed = true
  nil
end

#closed?Boolean

Returns true if the ZIP archive is closed, false otherwise.

Returns:

  • (Boolean)


185
186
187
# File 'lib/archive/zip.rb', line 185

def closed?
  @closed
end

#each(&b) ⇒ Object

Iterates through each entry of a readable ZIP archive in turn yielding each one to the given block.

Raises Archive::Zip::IOError if called on a non-readable archive or after the archive is closed.

Raises:



204
205
206
207
208
209
210
211
212
213
# File 'lib/archive/zip.rb', line 204

def each(&b)
  raise IOError, 'non-readable archive' unless readable?
  raise IOError, 'closed archive' if closed?

  unless @parse_complete then
    parse(@archive)
    @parse_complete = true
  end
  @entries.each(&b)
end

#extract(destination, options = {}) ⇒ Object

Extracts the contents of the archive to destination, where destination is a path to a directory which will contain the contents of the archive. The destination path will be created if it does not already exist.

options is a Hash optionally containing the following: :directories::

When set to +true+ (the default), entries representing directories in
the archive are extracted.  This happens after all non-directory entries
are extracted so that directory  can be properly updated.

:symlinks::

When set to +false+ (the default), entries representing symlinks in the
archive are skipped.  When set to +true+, such entries are extracted.
Exceptions may be raised on plaforms/file systems which do not support
symlinks.

:overwrite::

When set to <tt>:all</tt> (the default), files which already exist will
be replaced.  When set to <tt>:older</tt>, such files will only be
replaced if they are older according to their last modified times than
the zip entry which would replace them.  When set to <tt>:none</tt>,
such files will never be replaced.  Any other value is the same as
<tt>:all</tt>.

:create::

When set to +true+ (the default), files and directories which do not
already exist will be extracted.  When set to +false+, only files and
directories which already exist will be extracted (depending on the
setting of <b>:overwrite</b>).

:flatten::

When set to +false+ (the default), the directory paths containing
extracted files will be created within +destination+ in order to contain
the files.  When set to +true+, files are extracted directly to
+destination+ and directory entries are skipped.

:exclude::

Specifies a proc or lambda which takes a single argument containing a
zip entry and returns +true+ if the entry should be skipped during
extraction and +false+ if it should be extracted.

:password::

Specifies a proc, lambda, or a String.  If a proc or lambda is used, it
must take a single argument containing a zip entry and return a String
to be used as a decryption key for the entry.  If a String is used, it
will be used as a decryption key for all encrypted entries.

:on_error::

Specifies a proc or lambda which is called when an exception is raised
during the extraction of an entry.  It takes two arguments, a zip entry
and an exception object generated while attempting to extract the entry.
If <tt>:retry</tt> is returned, extraction of the entry is attempted
again.  If <tt>:skip</tt> is returned, the entry is skipped.  Otherwise,
the exception is raised.

Any other options which are supported by Archive::Zip::Entry#extract are also supported.

Raises Archive::Zip::IOError if called on a non-readable archive or after the archive is closed.

Example

An archive, archive.zip, contains:

zip-test/
zip-test/dir1/
zip-test/dir1/file2.txt
zip-test/dir2/
zip-test/file1.txt

A directory, extract4, contains:

zip-test
+- dir1
+- file1.txt

Extract the archive:

Archive::Zip.open('archive.zip') do |z|
z.extract('extract1')
end

Archive::Zip.open('archive.zip') do |z|
z.extract('extract2', :flatten => true)
end

Archive::Zip.open('archive.zip') do |z|
z.extract('extract3', :create => false)
end

Archive::Zip.open('archive.zip') do |z|
z.extract('extract3', :create => true)
end

Archive::Zip.open('archive.zip') do |z|
z.extract( 'extract5', :exclude => lambda { |e| e.file? })
end

The directories contain:

extract1 -> zip-test
          +- dir1
          |  +- file2.txt
          +- dir2
          +- file1.txt

extract2 -> file2.txt
          file1.txt

extract3 -> <empty>

extract4 -> zip-test
          +- dir2
          +- file1.txt       <- from archive contents

extract5 -> zip-test
          +- dir1
          +- dir2

Raises:



557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/archive/zip.rb', line 557

def extract(destination, options = {})
  raise IOError, 'non-readable archive' unless readable?
  raise IOError, 'closed archive' if closed?

  # Ensure that unspecified options have default values.
  options[:directories] = true  unless options.has_key?(:directories)
  options[:symlinks]    = false unless options.has_key?(:symlinks)
  options[:overwrite]   = :all  unless options[:overwrite] == :older ||
                                       options[:overwrite] == :never
  options[:create]      = true  unless options.has_key?(:create)
  options[:flatten]     = false unless options.has_key?(:flatten)

  # Flattening the archive structure implies that directory entries are
  # skipped.
  options[:directories] = false if options[:flatten]

  # First extract all non-directory entries.
  directories = []
  each do |entry|
    # Compute the target file path.
    file_path = entry.zip_path
    file_path = File.basename(file_path) if options[:flatten]
    file_path = File.join(destination, file_path)

    # Cache some information about the file path.
    file_exists = File.exist?(file_path)
    file_mtime = File.mtime(file_path) if file_exists

    begin
      # Skip this entry if so directed.
      if (! file_exists && ! options[:create]) ||
         (file_exists &&
          (options[:overwrite] == :never ||
           options[:overwrite] == :older && entry.mtime <= file_mtime)) ||
         (! options[:exclude].nil? && options[:exclude][entry]) then
        next
      end

      # Set the decryption key for the entry.
      if options[:password].kind_of?(String) then
        entry.password = options[:password]
      elsif ! options[:password].nil? then
        entry.password = options[:password][entry]
      end

      if entry.directory? then
        # Record the directories as they are encountered.
        directories << entry
      elsif entry.file? || (entry.symlink? && options[:symlinks]) then
        # Extract files and symlinks.
        entry.extract(
          options.merge(:file_path => file_path)
        )
      end
    rescue StandardError => error
      unless options[:on_error].nil? then
        case options[:on_error][entry, error]
        when :retry
          retry
        when :skip
        else
          raise
        end
      else
        raise
      end
    end
  end

  if options[:directories] then
    # Then extract the directory entries in depth first order so that time
    # stamps, ownerships, and permissions can be properly restored.
    directories.sort { |a, b| b.zip_path <=> a.zip_path }.each do |entry|
      begin
        entry.extract(
          options.merge(
            :file_path => File.join(destination, entry.zip_path)
          )
        )
      rescue StandardError => error
        unless options[:on_error].nil? then
          case options[:on_error][entry, error]
          when :retry
            retry
          when :skip
          else
            raise
          end
        else
          raise
        end
      end
    end
  end

  nil
end

#readable?Boolean

Returns true if the ZIP archive is readable, false otherwise.

Returns:

  • (Boolean)


190
191
192
# File 'lib/archive/zip.rb', line 190

def readable?
  @mode == :r
end

#writable?Boolean

Returns true if the ZIP archive is writable, false otherwise.

Returns:

  • (Boolean)


195
196
197
# File 'lib/archive/zip.rb', line 195

def writable?
  @mode == :w
end