Class: Archive::Zip

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.

Child modules and classes

Module Archive::Zip::Codec
Module Archive::Zip::Entry
Module Archive::Zip::ExtraField
Class Archive::Zip::DataDescriptor
Class Archive::Zip::EntryError
Class Archive::Zip::Error
Class Archive::Zip::ExtraFieldError
Class Archive::Zip::IOError
Class Archive::Zip::UnzipError

Constants

NameValue
EOCD_SIGNATURE "PK\x5\x6"
DS_SIGNATURE "PK\x5\x5"
Z64EOCD_SIGNATURE "PK\x6\x6"
Z64EOCDL_SIGNATURE "PK\x6\x7"
CFH_SIGNATURE "PK\x1\x2"
LFH_SIGNATURE "PK\x3\x4"
DD_SIGNATURE "PK\x7\x8"

Attributes

NameRead/write?
comment RW

Public Class Methods


archive (archive_path, paths, options = {})

A convenience method which opens a new or existing archive located in the path indicated by archive_path, adds and updates entries based on the paths given in paths, and then saves and closes the archive. See the instance method archive for more information about paths and options.

    # File lib/archive/zip.rb, line 51
51:     def self.archive(archive_path, paths, options = {})
52:       open(archive_path) { |z| z.archive(paths, options) }
53:     end

extract (archive_path, destination, options = {})

A convenience method which opens an archive located in the path indicated by archive_path, extracts the entries to the path indicated by destination, and then closes the archive. See the instance method extract for more information about destination and options.

    # File lib/archive/zip.rb, line 59
59:     def self.extract(archive_path, destination, options = {})
60:       open(archive_path) { |z| z.extract(destination, options) }
61:     end

new (archive_path, archive_out = nil)

Open and parse the file located at the path indicated by archive_path if archive_path is not nil and the path exists. If archive_out is unspecified or nil, any changes made will be saved in place, replacing the current archive with a new one having the same name. If archive_out is a String, it points to a file which will recieve the new archive‘s contents. Otherwise, archive_out is assumed to be a writable, IO-like object operating in binary mode which will recieve the new archive‘s contents.

At least one of archive_path and archive_out must be specified and non-nil; otherwise, an error will be raised.

     # File lib/archive/zip.rb, line 90
 90:     def initialize(archive_path, archive_out = nil)
 91:       if (archive_path.nil? || archive_path.empty?) &&
 92:          (archive_out.nil? ||
 93:           archive_out.kind_of?(String) && archive_out.empty?) then
 94:         raise ArgumentError, 'No valid source or destination archive specified'
 95:       end
 96:       @archive_path = archive_path
 97:       @archive_out = archive_out
 98:       @entries = {}
 99:       @dirty = false
100:       @comment = ''
101:       @closed = false
102:       if ! @archive_path.nil? && File.exist?(@archive_path) then
103:         @archive_in = File.new(@archive_path, 'rb')
104:         parse(@archive_in)
105:       end
106:     end

open (archive_path, archive_out = nil) {|zf| ...}

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.

    # File lib/archive/zip.rb, line 68
68:     def self.open(archive_path, archive_out = nil)
69:       zf = new(archive_path, archive_out)
70:       return zf unless block_given?
71: 
72:       begin
73:         yield(zf)
74:       ensure
75:         zf.close unless zf.closed?
76:       end
77:     end

Public Instance Methods




add_entry (entry)

Add entry into the ZIP archive replacing any existing entry with the same zip path.

Raises Archive::Zip::IOError if called after close.

     # File lib/archive/zip.rb, line 176
176:     def add_entry(entry)
177:       raise IOError, 'closed archive' if @closed
178:       unless entry.kind_of?(Entry) then
179:         raise ArgumentError, 'Archive::Zip::Entry instance required'
180:       end
181: 
182:       @entries[entry.zip_path] = entry
183:       @dirty = true
184:       self
185:     end

archive (paths, options = {})

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 :recursion option is true.
:symlinks:When set to false (the default), entries for symlinks are excluded from the archive. Otherwise, they are included. NOTE: Unless :follow_symlinks 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 :follow_symlinks to false. See Archive::Zip::Entry.from_file for details regarding :follow_symlinks.
: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 :directories is false and :path_prefix 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. NOTE: If a directory is excluded in this way, the :recursion 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 :retry is returned, archival of the entry is attempted again. If :skip 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.

Raises Archive::Zip::IOError if called after close. 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/
     # File lib/archive/zip.rb, line 319
319:     def archive(paths, options = {})
320:       raise IOError, 'closed archive' if @closed
321: 
322:       # Ensure that paths is an enumerable.
323:       paths = [paths] unless paths.kind_of?(Enumerable)
324:       # If the basename of a path is '.' or '..', replace the path with the
325:       # paths of all the entries contained within the directory referenced by
326:       # the original path.
327:       paths = paths.collect do |path|
328:         basename = File.basename(path)
329:         if basename == '.' || basename == '..' then
330:           Dir.entries(path).reject do |e|
331:             e == '.' || e == '..'
332:           end.collect do |e|
333:             File.join(path, e)
334:           end
335:         else
336:           path
337:         end
338:       end.flatten.uniq
339: 
340:       # Ensure that unspecified options have default values.
341:       options[:path_prefix]  = ''    unless options.has_key?(:path_prefix)
342:       options[:recursion]    = true  unless options.has_key?(:recursion)
343:       options[:directories]  = true  unless options.has_key?(:directories)
344:       options[:symlinks]     = false unless options.has_key?(:symlinks)
345:       options[:flatten]      = false unless options.has_key?(:flatten)
346: 
347:       # Flattening the directory structure implies that directories are skipped
348:       # and that the path prefix should be ignored.
349:       if options[:flatten] then
350:         options[:path_prefix] = ''
351:         options[:directories] = false
352:       end
353: 
354:       # Clean up the path prefix.
355:       options[:path_prefix] = Entry.expand_path(options[:path_prefix].to_s)
356: 
357:       paths.each do |path|
358:         # Generate the zip path.
359:         zip_entry_path = File.basename(path)
360:         zip_entry_path += '/' if File.directory?(path)
361:         unless options[:path_prefix].empty? then
362:           zip_entry_path = "#{options[:path_prefix]}/#{zip_entry_path}"
363:         end
364: 
365:         begin
366:           # Create the entry, but do not add it to the archive yet.
367:           zip_entry = Zip::Entry.from_file(
368:             path,
369:             options.merge(
370:               :zip_path        => zip_entry_path,
371:               :follow_symlinks => options.has_key?(:follow_symlinks) ?
372:                                   options[:follow_symlinks] :
373:                                   ! options[:symlinks]
374:             )
375:           )
376:         rescue StandardError => error
377:           unless options[:on_error].nil? then
378:             case options[:on_error][path, error]
379:             when :retry
380:               retry
381:             when :skip
382:               next
383:             else
384:               raise
385:             end
386:           else
387:             raise
388:           end
389:         end
390: 
391:         # Skip this entry if so directed.
392:         if (zip_entry.symlink? && ! options[:symlinks]) ||
393:            (! options[:exclude].nil? && options[:exclude][zip_entry]) then
394:           next
395:         end
396: 
397:         # Set the encryption key for the entry.
398:         if options[:password].kind_of?(String) then
399:           zip_entry.password = options[:password]
400:         elsif ! options[:password].nil? then
401:           zip_entry.password = options[:password][zip_entry]
402:         end
403: 
404:         # Add entries for directories (if requested) and files/symlinks.
405:         if (! zip_entry.directory? || options[:directories]) then
406:           add_entry(zip_entry)
407:         end
408: 
409:         # Recurse into subdirectories (if requested).
410:         if zip_entry.directory? && options[:recursion] then
411:           archive(
412:             Dir.entries(path).reject do |e|
413:               e == '.' || e == '..'
414:             end.collect do |e|
415:               File.join(path, e)
416:             end,
417:             options.merge(:path_prefix => zip_entry_path)
418:           )
419:         end
420:       end
421: 
422:       nil
423:     end

close ()

Close the archive. It is at this point that any changes made to the archive will be persisted to an output stream.

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

     # File lib/archive/zip.rb, line 115
115:     def close
116:       raise IOError, 'closed archive' if closed?
117: 
118:       if @dirty then
119:         # There is something to write...
120:         if @archive_out.nil? then
121:           # Update the archive "in place".
122:           tmp_archive_path = nil
123:           Tempfile.open(*File.split(@archive_path).reverse) do |archive_out|
124:             # Ensure the file is in binary mode for Windows.
125:             archive_out.binmode
126:             # Save off the path so that the temporary file can be renamed to the
127:             # archive file later.
128:             tmp_archive_path = archive_out.path
129:             dump(archive_out)
130:           end
131:           File.chmod(0666 & ~File.umask, tmp_archive_path)
132:         elsif @archive_out.kind_of?(String) then
133:           # Open a new archive to receive the data.
134:           File.open(@archive_out, 'wb') do |archive_out|
135:             dump(archive_out)
136:           end
137:         else
138:           # Assume the given object is an IO-like object and dump the archive
139:           # contents to it.
140:           dump(@archive_out)
141:         end
142:         @archive_in.close unless @archive_in.nil?
143:         # The rename must happen after the original archive is closed when
144:         # running on Windows since that platform does not allow a file which is
145:         # in use to be replaced as is required when trying to update the archive
146:         # "in place".
147:         File.rename(tmp_archive_path, @archive_path) if @archive_out.nil?
148:       elsif ! @archive_in.nil? then
149:         @archive_in.close
150:       end
151: 
152:       closed = true
153:       nil
154:     end

closed? ()

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

     # File lib/archive/zip.rb, line 157
157:     def closed?
158:       @closed
159:     end

each (&b)

When the ZIP archive is open, this method iterates through each entry in turn yielding each one to the given block. Since Zip includes Enumerable, Zip instances are enumerables of Entry instances.

Raises Archive::Zip::IOError if called after close.

     # File lib/archive/zip.rb, line 166
166:     def each(&b)
167:       raise IOError, 'closed archive' if @closed
168: 
169:       @entries.each_value(&b)
170:     end

extract (destination, options = {})

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 metadata 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 :all (the default), files which already exist will be replaced. When set to :older, 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 :none, such files will never be replaced. Any other value is the same as :all.
: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 :overwrite).
: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 :retry is returned, extraction of the entry is attempted again. If :skip 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 after close.

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
     # File lib/archive/zip.rb, line 531
531:     def extract(destination, options = {})
532:       raise IOError, 'closed archive' if @closed
533: 
534:       # Ensure that unspecified options have default values.
535:       options[:directories] = true  unless options.has_key?(:directories)
536:       options[:symlinks]    = false unless options.has_key?(:symlinks)
537:       options[:overwrite]   = :all  unless options[:overwrite] == :older ||
538:                                            options[:overwrite] == :never
539:       options[:create]      = true  unless options.has_key?(:create)
540:       options[:flatten]     = false unless options.has_key?(:flatten)
541: 
542:       # Flattening the archive structure implies that directory entries are
543:       # skipped.
544:       options[:directories] = false if options[:flatten]
545: 
546:       # First extract all non-directory entries.
547:       directories = []
548:       each do |entry|
549:         # Compute the target file path.
550:         file_path = entry.zip_path
551:         file_path = File.basename(file_path) if options[:flatten]
552:         file_path = File.join(destination, file_path)
553: 
554:         # Cache some information about the file path.
555:         file_exists = File.exist?(file_path)
556:         file_mtime = File.mtime(file_path) if file_exists
557: 
558:         begin
559:           # Skip this entry if so directed.
560:           if (! file_exists && ! options[:create]) ||
561:              (file_exists &&
562:               (options[:overwrite] == :never ||
563:                options[:overwrite] == :older && entry.mtime <= file_mtime)) ||
564:              (! options[:exclude].nil? && options[:exclude][entry]) then
565:             next
566:           end
567: 
568:           # Set the decryption key for the entry.
569:           if options[:password].kind_of?(String) then
570:             entry.password = options[:password]
571:           elsif ! options[:password].nil? then
572:             entry.password = options[:password][entry]
573:           end
574: 
575:           if entry.directory? then
576:             # Record the directories as they are encountered.
577:             directories << entry
578:           elsif entry.file? || (entry.symlink? && options[:symlinks]) then
579:             # Extract files and symlinks.
580:             entry.extract(
581:               options.merge(:file_path => file_path)
582:             )
583:           end
584:         rescue StandardError => error
585:           unless options[:on_error].nil? then
586:             case options[:on_error][entry, error]
587:             when :retry
588:               retry
589:             when :skip
590:             else
591:               raise
592:             end
593:           else
594:             raise
595:           end
596:         end
597:       end
598: 
599:       if options[:directories] then
600:         # Then extract the directory entries in depth first order so that time
601:         # stamps, ownerships, and permissions can be properly restored.
602:         directories.sort { |a, b| b.zip_path <=> a.zip_path }.each do |entry|
603:           begin
604:             entry.extract(
605:               options.merge(
606:                 :file_path => File.join(destination, entry.zip_path)
607:               )
608:             )
609:           rescue StandardError => error
610:             unless options[:on_error].nil? then
611:               case options[:on_error][entry, error]
612:               when :retry
613:                 retry
614:               when :skip
615:               else
616:                 raise
617:               end
618:             else
619:               raise
620:             end
621:           end
622:         end
623:       end
624: 
625:       nil
626:     end

get_entry (zip_path)

Look up an entry based on the zip path located in zip_path. Returns nil if no entry is found.

     # File lib/archive/zip.rb, line 190
190:     def get_entry(zip_path)
191:       @entries[zip_path]
192:     end

remove_entry (entry)

Removes an entry from the ZIP file and returns the entry or nil if no entry was found to remove. If entry is an instance of Archive::Zip::Entry, the zip_path attribute is used to find the entry to remove; otherwise, entry is assumed to be a zip path matching an entry in the ZIP archive.

Raises Archive::Zip::IOError if called after close.

     # File lib/archive/zip.rb, line 202
202:     def remove_entry(entry)
203:       raise IOError, 'closed archive' if @closed
204: 
205:       zip_path = entry
206:       zip_path = entry.zip_path if entry.kind_of?(Entry)
207:       entry = @entries.delete(zip_path)
208:       entry = entry[1] unless entry.nil?
209:       @dirty ||= ! entry.nil?
210:       entry
211:     end