2010-11-30 16:27:59 +08:00
|
|
|
module Zip
|
2011-01-07 21:23:22 +08:00
|
|
|
# ZipOutputStream is the basic class for writing zip files. It is
|
|
|
|
# possible to create a ZipOutputStream object directly, passing
|
|
|
|
# the zip file name to the constructor, but more often than not
|
|
|
|
# the ZipOutputStream will be obtained from a ZipFile (perhaps using the
|
|
|
|
# ZipFileSystem interface) object for a particular entry in the zip
|
2010-11-30 16:27:59 +08:00
|
|
|
# archive.
|
|
|
|
#
|
2011-01-07 21:23:22 +08:00
|
|
|
# A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
|
|
|
|
# to provide an IO-like interface for writing to a single zip
|
|
|
|
# entry. Beyond methods for mimicking an IO-object it contains
|
|
|
|
# the method put_next_entry that closes the current entry
|
2010-11-30 16:27:59 +08:00
|
|
|
# and creates a new.
|
|
|
|
#
|
|
|
|
# Please refer to ZipInputStream for example code.
|
|
|
|
#
|
2011-01-07 21:23:22 +08:00
|
|
|
# java.util.zip.ZipOutputStream is the original inspiration for this
|
2010-11-30 16:27:59 +08:00
|
|
|
# class.
|
|
|
|
|
2013-06-03 15:56:24 +08:00
|
|
|
class OutputStream
|
2013-06-03 02:33:03 +08:00
|
|
|
include ::Zip::IOExtras::AbstractOutputStream
|
2010-11-30 16:27:59 +08:00
|
|
|
|
|
|
|
attr_accessor :comment
|
|
|
|
|
|
|
|
# Opens the indicated zip file. If a file with that name already
|
|
|
|
# exists it will be overwritten.
|
2014-01-19 19:45:58 +08:00
|
|
|
def initialize(file_name, stream=false)
|
2010-11-30 16:27:59 +08:00
|
|
|
super()
|
2014-01-19 19:45:58 +08:00
|
|
|
@file_name = file_name
|
|
|
|
@output_stream = if stream
|
2014-01-19 20:06:54 +08:00
|
|
|
iostream = @file_name.dup
|
|
|
|
iostream.reopen
|
|
|
|
iostream.rewind
|
|
|
|
iostream
|
2014-01-19 19:45:58 +08:00
|
|
|
else
|
|
|
|
::File.new(@file_name, "wb")
|
|
|
|
end
|
2013-06-03 15:56:24 +08:00
|
|
|
@entry_set = ::Zip::EntrySet.new
|
|
|
|
@compressor = ::Zip::NullCompressor.instance
|
2010-11-30 16:27:59 +08:00
|
|
|
@closed = false
|
2013-08-30 04:50:12 +08:00
|
|
|
@current_entry = nil
|
2010-11-30 16:27:59 +08:00
|
|
|
@comment = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
# Same as #initialize but if a block is passed the opened
|
|
|
|
# stream is passed to the block and closed when the block
|
2011-01-07 21:23:22 +08:00
|
|
|
# returns.
|
2013-06-03 15:56:24 +08:00
|
|
|
class << self
|
2014-01-19 19:45:58 +08:00
|
|
|
def open(file_name)
|
|
|
|
return new(file_name) unless block_given?
|
|
|
|
zos = new(file_name)
|
2013-06-03 15:56:24 +08:00
|
|
|
yield zos
|
|
|
|
ensure
|
|
|
|
zos.close if zos
|
|
|
|
end
|
2010-11-30 16:27:59 +08:00
|
|
|
|
2014-01-19 19:45:58 +08:00
|
|
|
# Same as #open but writes to a filestream instead
|
2014-04-06 21:45:19 +08:00
|
|
|
def write_buffer(io = ::StringIO.new(''))
|
2014-01-19 19:45:58 +08:00
|
|
|
zos = new(io, true)
|
2013-06-03 15:56:24 +08:00
|
|
|
yield zos
|
2014-01-19 19:45:58 +08:00
|
|
|
zos.close_buffer
|
2013-06-03 15:56:24 +08:00
|
|
|
end
|
2011-01-07 21:23:22 +08:00
|
|
|
end
|
|
|
|
|
2010-11-30 16:27:59 +08:00
|
|
|
# Closes the stream and writes the central directory to the zip file
|
|
|
|
def close
|
|
|
|
return if @closed
|
|
|
|
finalize_current_entry
|
|
|
|
update_local_headers
|
|
|
|
write_central_directory
|
2013-06-03 02:33:03 +08:00
|
|
|
@output_stream.close
|
2010-11-30 16:27:59 +08:00
|
|
|
@closed = true
|
|
|
|
end
|
|
|
|
|
2011-01-07 21:23:22 +08:00
|
|
|
# Closes the stream and writes the central directory to the zip file
|
|
|
|
def close_buffer
|
2013-06-03 02:33:03 +08:00
|
|
|
return @output_stream if @closed
|
2011-01-07 21:23:22 +08:00
|
|
|
finalize_current_entry
|
|
|
|
update_local_headers
|
|
|
|
write_central_directory
|
|
|
|
@closed = true
|
2013-06-03 02:33:03 +08:00
|
|
|
@output_stream
|
2011-01-07 21:23:22 +08:00
|
|
|
end
|
|
|
|
|
2014-01-19 19:45:58 +08:00
|
|
|
# Closes the current entry and opens a new for writing.
|
2010-11-30 16:27:59 +08:00
|
|
|
# +entry+ can be a ZipEntry object or a string.
|
2014-01-24 17:37:38 +08:00
|
|
|
def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression)
|
|
|
|
raise Error, "zip stream is closed" if @closed
|
|
|
|
if entry_name.kind_of?(Entry)
|
|
|
|
new_entry = entry_name
|
2012-03-27 17:31:13 +08:00
|
|
|
else
|
2014-01-24 17:37:38 +08:00
|
|
|
new_entry = Entry.new(@file_name, entry_name.to_s)
|
2012-03-27 17:31:13 +08:00
|
|
|
end
|
2014-01-24 17:37:38 +08:00
|
|
|
new_entry.comment = comment unless comment.nil?
|
|
|
|
unless extra.nil?
|
2013-06-03 15:56:24 +08:00
|
|
|
new_entry.extra = ExtraField === extra ? extra : ExtraField.new(extra.to_s)
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
2014-01-24 17:37:38 +08:00
|
|
|
new_entry.compression_method = compression_method unless compression_method.nil?
|
2010-11-30 16:27:59 +08:00
|
|
|
init_next_entry(new_entry, level)
|
2013-08-30 04:50:12 +08:00
|
|
|
@current_entry = new_entry
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def copy_raw_entry(entry)
|
|
|
|
entry = entry.dup
|
2014-01-24 17:37:38 +08:00
|
|
|
raise Error, "zip stream is closed" if @closed
|
|
|
|
raise Error, "entry is not a ZipEntry" unless entry.is_a?(Entry)
|
2010-11-30 16:27:59 +08:00
|
|
|
finalize_current_entry
|
2013-06-03 02:33:03 +08:00
|
|
|
@entry_set << entry
|
2014-03-13 05:42:53 +08:00
|
|
|
src_pos = entry.local_header_offset
|
2013-06-03 02:33:03 +08:00
|
|
|
entry.write_local_entry(@output_stream)
|
2010-11-30 16:27:59 +08:00
|
|
|
@compressor = NullCompressor.instance
|
2012-02-06 08:59:11 +08:00
|
|
|
entry.get_raw_input_stream do |is|
|
2011-11-17 17:48:42 +08:00
|
|
|
is.seek(src_pos, IO::SEEK_SET)
|
2014-03-13 05:42:53 +08:00
|
|
|
::Zip::Entry.read_local_entry(is)
|
2013-06-03 02:33:03 +08:00
|
|
|
IOExtras.copy_stream_n(@output_stream, is, entry.compressed_size)
|
2012-02-06 08:59:11 +08:00
|
|
|
end
|
2010-11-30 16:27:59 +08:00
|
|
|
@compressor = NullCompressor.instance
|
2013-08-30 04:50:12 +08:00
|
|
|
@current_entry = nil
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
2012-02-14 06:03:34 +08:00
|
|
|
|
2010-11-30 16:27:59 +08:00
|
|
|
def finalize_current_entry
|
2013-08-30 04:50:12 +08:00
|
|
|
return unless @current_entry
|
2010-11-30 16:27:59 +08:00
|
|
|
finish
|
2013-08-30 04:50:12 +08:00
|
|
|
@current_entry.compressed_size = @output_stream.tell - @current_entry.local_header_offset - @current_entry.calculate_local_header_size
|
|
|
|
@current_entry.size = @compressor.size
|
|
|
|
@current_entry.crc = @compressor.crc
|
|
|
|
@current_entry = nil
|
2014-01-24 17:37:38 +08:00
|
|
|
@compressor = ::Zip::NullCompressor.instance
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
2011-01-07 21:23:22 +08:00
|
|
|
|
2014-01-20 21:38:22 +08:00
|
|
|
def init_next_entry(entry, level = Zip.default_compression)
|
2010-11-30 16:27:59 +08:00
|
|
|
finalize_current_entry
|
2013-06-03 02:33:03 +08:00
|
|
|
@entry_set << entry
|
|
|
|
entry.write_local_entry(@output_stream)
|
2010-11-30 16:27:59 +08:00
|
|
|
@compressor = get_compressor(entry, level)
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_compressor(entry, level)
|
|
|
|
case entry.compression_method
|
2014-01-19 19:45:58 +08:00
|
|
|
when Entry::DEFLATED then
|
2014-01-24 17:37:38 +08:00
|
|
|
::Zip::Deflater.new(@output_stream, level)
|
2014-01-19 19:45:58 +08:00
|
|
|
when Entry::STORED then
|
2014-01-24 17:37:38 +08:00
|
|
|
::Zip::PassThruCompressor.new(@output_stream)
|
2014-01-19 19:45:58 +08:00
|
|
|
else
|
2014-01-24 17:37:38 +08:00
|
|
|
raise ::Zip::CompressionMethodError,
|
2014-01-19 19:45:58 +08:00
|
|
|
"Invalid compression method: '#{entry.compression_method}'"
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_local_headers
|
2013-06-03 02:33:03 +08:00
|
|
|
pos = @output_stream.pos
|
|
|
|
@entry_set.each do |entry|
|
|
|
|
@output_stream.pos = entry.local_header_offset
|
Add read/write support for zip64 extensions
This commit adds the capability of creating archives larger than
4GB via zip64 extensions. It also fixes bugs reading archives of
this size (specifically, the 64-bit offset of the local file
header was not being read from the central directory entry).
To maximize compatibility, zip64 extensions are used only when
required. Unfortunately, at the time we write a local file header,
we don't know the size of the file and thus whether a Zip64
Extended Information Extra Field will be required. Therefore
this commit writes a 'placeholder' extra field to reserve space
for the zip64 entry, which will be written if necessary when
we update the local entry with the final sizes and CRC. I use
the signature "\x99\x99" for this field, following the example
of DotNetZip which does the same.
This commit also adds a rake task, zip64_full_test, which
fully tests zip64 by actually creating and verifying a 4GB zip
file. Please note, however, that this test requires UnZip
version 6.00 or newer, which may not be supplied by your OS.
This test doesn't run along with the main unit tests because
it takes a few minutes to complete.
2013-09-28 10:41:00 +08:00
|
|
|
entry.write_local_entry(@output_stream, true)
|
2012-02-14 06:03:34 +08:00
|
|
|
end
|
2013-06-03 02:33:03 +08:00
|
|
|
@output_stream.pos = pos
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def write_central_directory
|
2013-06-03 15:56:24 +08:00
|
|
|
cdir = CentralDirectory.new(@entry_set, @comment)
|
2013-06-03 02:33:03 +08:00
|
|
|
cdir.write_to_stream(@output_stream)
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
|
|
|
def finish
|
|
|
|
@compressor.finish
|
|
|
|
end
|
|
|
|
|
|
|
|
public
|
2014-03-01 06:31:10 +08:00
|
|
|
|
2010-11-30 16:27:59 +08:00
|
|
|
# Modeled after IO.<<
|
|
|
|
def << (data)
|
|
|
|
@compressor << data
|
2014-03-01 06:31:10 +08:00
|
|
|
self
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
2014-03-01 06:31:10 +08:00
|
|
|
|
2010-11-30 16:27:59 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
|
|
|
# rubyzip is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the ruby license.
|