rubyzip

Gem Version Tests Linter Code Climate Coverage Status

Rubyzip is a ruby library for reading and writing zip files.

Important notes

Updating to version 3.0

The public API of some classes has been modernized to use named parameters for optional arguments. Please check your usage of the following Rubyzip classes:

  • File
  • Entry
  • InputStream
  • OutputStream

Please see Updating to version 3.x in the wiki for details.

Requirements

Version 3.x requires at least Ruby 3.0.

Version 2.x requires at least Ruby 2.4, and is known to work on Ruby 3.x.

It is not recommended to use any versions of Rubyzip earlier than 2.3 due to security issues.

Installation

Rubyzip is available on RubyGems:

gem install rubyzip

Or in your Gemfile:

gem 'rubyzip'

Usage

Basic zip archive creation

require 'rubygems'
require 'zip'

folder = "Users/me/Desktop/stuff_to_zip"
input_filenames = ['image.jpg', 'description.txt', 'stats.csv']

zipfile_name = "/Users/me/Desktop/archive.zip"

Zip::File.open(zipfile_name, create: true) do |zipfile|
  input_filenames.each do |filename|
    # Two arguments:
    # - The name of the file as it will appear in the archive
    # - The original file, including the path to find it
    zipfile.add(filename, File.join(folder, filename))
  end
  zipfile.get_output_stream("myFile") { |f| f.write "myFile contains just this" }
end

Zipping a directory recursively

Copy from here

require 'zip'

# This is a simple example which uses rubyzip to
# recursively generate a zip file from the contents of
# a specified directory. The directory itself is not
# included in the archive, rather just its contents.
#
# Usage:
#   directory_to_zip = "/tmp/input"
#   output_file = "/tmp/out.zip"
#   zf = ZipFileGenerator.new(directory_to_zip, output_file)
#   zf.write()
class ZipFileGenerator
  # Initialize with the directory to zip and the location of the output archive.
  def initialize(input_dir, output_file)
    @input_dir = input_dir
    @output_file = output_file
  end

  # Zip the input directory.
  def write
    entries = Dir.entries(@input_dir) - %w[. ..]

    ::Zip::File.open(@output_file, create: true) do |zipfile|
      write_entries entries, '', zipfile
    end
  end

  private

  # A helper method to make the recursion work.
  def write_entries(entries, path, zipfile)
    entries.each do |e|
      zipfile_path = path == '' ? e : File.join(path, e)
      disk_file_path = File.join(@input_dir, zipfile_path)

      if File.directory? disk_file_path
        recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
      else
        put_into_archive(disk_file_path, zipfile, zipfile_path)
      end
    end
  end

  def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
    zipfile.mkdir zipfile_path
    subdir = Dir.entries(disk_file_path) - %w[. ..]
    write_entries subdir, zipfile_path, zipfile
  end

  def put_into_archive(disk_file_path, zipfile, zipfile_path)
    zipfile.add(zipfile_path, disk_file_path)
  end
end

Save zip archive entries sorted by name

To save zip archives with their entries sorted by name (see below), set ::Zip.sort_entries to true

Vegetable/
Vegetable/bean
Vegetable/carrot
Vegetable/celery
fruit/
fruit/apple
fruit/kiwi
fruit/mango
fruit/orange

Opening an existing zip file with this option set will not change the order of the entries automatically. Altering the zip file - adding an entry, renaming an entry, adding or changing the archive comment, etc - will cause the ordering to be applied when closing the file.

Default permissions of zip archives

On Posix file systems the default file permissions applied to a new archive are (0666 - umask), which mimics the behavior of standard tools such as touch.

On Windows the default file permissions are set to 0644 as suggested by the Ruby File documentation.

When modifying a zip archive the file permissions of the archive are preserved.

Reading a Zip file

MAX_SIZE = 1024**2 # 1MiB (but of course you can increase this)
Zip::File.open('foo.zip') do |zip_file|
  # Handle entries one by one
  zip_file.each do |entry|
    puts "Extracting #{entry.name}"
    raise 'File too large when extracted' if entry.size > MAX_SIZE

    # Extract to file or directory based on name in the archive
    entry.extract

    # Read into memory
    content = entry.get_input_stream.read
  end

  # Find specific entry
  entry = zip_file.glob('*.csv').first
  raise 'File too large when extracted' if entry.size > MAX_SIZE
  puts entry.get_input_stream.read
end

Notes on Zip::InputStream

Zip::InputStream can be used for faster reading of zip file content because it does not read the Central directory up front.

There is one exception where it can not work however, and this is if the file does not contain enough information in the local entry headers to extract an entry. This is indicated in an entry by the General Purpose Flag bit 3 being set.

If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data.

If Zip::InputStream finds such an entry in the zip archive it will raise an exception (Zip::StreamingError).

Zip::InputStream is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via Zip::InputStream.get_next_entry then you should complete any such operations before the next call to get_next_entry.

zip_stream = Zip::InputStream.new(File.open('file.zip'))

while entry = zip_stream.get_next_entry
  # All required operations on `entry` go here.
end

Any attempt to move about in a zip file opened with Zip::InputStream could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use Zip::File.

Password Protection (Experimental)

Rubyzip supports reading/writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). AES encryption is not yet supported. It can be used with buffer streams, e.g.:

Version 2.x

# Writing.
enc = Zip::TraditionalEncrypter.new('password')
buffer = Zip::OutputStream.write_buffer(::StringIO.new(''), enc) do |output|
  output.put_next_entry(