lib/archive/zip.rb
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
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
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 <tt>"."</tt> or
 <tt>".."</tt>.  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:
 <b>:path_prefix</b>::
   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.
 <b>:recursion</b>::
   When set to +true+ (the default), the contents of directories are
   recursively added to the archive.
 <b>:directories</b>::
   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+.
 <b>:symlinks</b>::
   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>.
 <b>:flatten</b>::
   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.
 <b>:exclude</b>::
   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.
 <b>:password</b>::
   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.
 <b>:on_error</b>::
   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.

 <b>NOTE:</b> 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
 <b>:on_error</b> 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/