rubyzip/lib/zip/output_stream.rb

209 lines
6.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
module Zip
# 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
# archive.
#
# 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
# and creates a new.
#
# Please refer to ZipInputStream for example code.
#
# java.util.zip.ZipOutputStream is the original inspiration for this
# class.
class OutputStream
2013-06-03 02:33:03 +08:00
include ::Zip::IOExtras::AbstractOutputStream
attr_accessor :comment
# Opens the indicated zip file. If a file with that name already
# exists it will be overwritten.
def initialize(file_name, stream: false, encrypter: nil)
super()
2014-01-19 19:45:58 +08:00
@file_name = file_name
@output_stream = if stream
Prevent directory not empty error when running file_test on Windows Fixed error: ZipFileTest#test_open_buffer_no_op_does_not_change_file: Errno::ENOTEMPTY: Directory not empty @ dir_s_rmdir - D:/a/_temp/d20210605-6612-1yi35sp C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1335:in `rmdir' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1335:in `block in remove_dir1' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1349:in `platform_support' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1334:in `remove_dir1' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1327:in `remove' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:689:in `block in remove_entry' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1384:in `ensure in postorder_traverse' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1384:in `postorder_traverse' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:687:in `remove_entry' C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/tmpdir.rb:101:in `mktmpdir' D:/a/rubyzip/rubyzip/test/file_test.rb:136:in `test_open_buffer_no_op_does_not_change_file' Rationale: File#dup does not behave like what you would expect from #dup on Ruby. File#dup calls dup(2), which has OS dependant behavoir. On Windows, calling File#dup seems to cause an extra reference to an open file, which prevents deleting that file later. With this commit, we leave out the call to File#dup on Windows. It is not clear to me that removing this call has no undesired consequences, but all other existing tests still succeed.
2021-06-04 23:51:09 +08:00
iostream = Zip::RUNNING_ON_WINDOWS ? @file_name : @file_name.dup
iostream.reopen(@file_name)
iostream.rewind
iostream
2014-01-19 19:45:58 +08:00
else
2015-03-21 16:27:44 +08:00
::File.new(@file_name, 'wb')
2014-01-19 19:45:58 +08:00
end
@entry_set = ::Zip::EntrySet.new
@compressor = ::Zip::NullCompressor.instance
@encrypter = encrypter || ::Zip::NullEncrypter.new
@closed = false
2013-08-30 04:50:12 +08:00
@current_entry = nil
@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
# returns.
class << self
def open(file_name, encrypter: nil)
2014-01-19 19:45:58 +08:00
return new(file_name) unless block_given?
zos = new(file_name, stream: false, encrypter: encrypter)
yield zos
ensure
zos.close if zos
end
2014-01-19 19:45:58 +08:00
# Same as #open but writes to a filestream instead
2021-06-09 01:09:22 +08:00
def write_buffer(io = ::StringIO.new, encrypter: nil)
2020-02-20 01:48:13 +08:00
io.binmode if io.respond_to?(:binmode)
zos = new(io, stream: true, encrypter: encrypter)
yield zos
2014-01-19 19:45:58 +08:00
zos.close_buffer
end
end
# 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
@closed = true
end
# 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
finalize_current_entry
update_local_headers
write_central_directory
@closed = true
@output_stream.flush
2013-06-03 02:33:03 +08:00
@output_stream
end
2014-01-19 19:45:58 +08:00
# Closes the current entry and opens a new for writing.
# +entry+ can be a ZipEntry object or a string.
def put_next_entry(
entry_name, comment = '', extra = ExtraField.new,
compression_method = Entry::DEFLATED, level = Zip.default_compression
)
2015-03-21 16:27:44 +08:00
raise Error, 'zip stream is closed' if @closed
2017-06-29 10:57:12 +08:00
new_entry = if entry_name.kind_of?(Entry)
entry_name
else
Entry.new(
@file_name, entry_name.to_s, comment: comment,
extra: extra, compression_method: compression_method,
compression_level: level
)
2017-06-29 10:57:12 +08:00
end
init_next_entry(new_entry)
2013-08-30 04:50:12 +08:00
@current_entry = new_entry
end
def copy_raw_entry(entry)
entry = entry.dup
2015-03-21 16:27:44 +08:00
raise Error, 'zip stream is closed' if @closed
raise Error, 'entry is not a ZipEntry' unless entry.kind_of?(Entry)
finalize_current_entry
2013-06-03 02:33:03 +08:00
@entry_set << entry
src_pos = entry.local_header_offset
2013-06-03 02:33:03 +08:00
entry.write_local_entry(@output_stream)
@compressor = NullCompressor.instance
entry.get_raw_input_stream do |is|
is.seek(src_pos, IO::SEEK_SET)
::Zip::Entry.read_local_entry(is)
2013-06-03 02:33:03 +08:00
IOExtras.copy_stream_n(@output_stream, is, entry.compressed_size)
end
@compressor = NullCompressor.instance
2013-08-30 04:50:12 +08:00
@current_entry = nil
end
private
2012-02-14 06:03:34 +08:00
def finalize_current_entry
2013-08-30 04:50:12 +08:00
return unless @current_entry
finish
@current_entry.compressed_size = @output_stream.tell - \
@current_entry.local_header_offset - \
@current_entry.calculate_local_header_size
2013-08-30 04:50:12 +08:00
@current_entry.size = @compressor.size
@current_entry.crc = @compressor.crc
@output_stream << @encrypter.data_descriptor(
@current_entry.crc,
@current_entry.compressed_size,
@current_entry.size
)
2014-12-31 00:03:17 +08:00
@current_entry.gp_flags |= @encrypter.gp_flags
2013-08-30 04:50:12 +08:00
@current_entry = nil
2014-01-24 17:37:38 +08:00
@compressor = ::Zip::NullCompressor.instance
end
def init_next_entry(entry)
finalize_current_entry
2013-06-03 02:33:03 +08:00
@entry_set << entry
entry.write_local_entry(@output_stream)
2014-12-31 00:03:17 +08:00
@encrypter.reset!
@output_stream << @encrypter.header(entry.mtime)
@compressor = get_compressor(entry)
end
def get_compressor(entry)
case entry.compression_method
2019-09-22 21:23:57 +08:00
when Entry::DEFLATED
::Zip::Deflater.new(@output_stream, entry.compression_level, @encrypter)
2019-09-22 21:23:57 +08:00
when Entry::STORED
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}'"
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
entry.write_local_entry(@output_stream, rewrite: true)
2012-02-14 06:03:34 +08:00
end
2013-06-03 02:33:03 +08:00
@output_stream.pos = pos
end
def write_central_directory
cdir = CentralDirectory.new(@entry_set, @comment)
2013-06-03 02:33:03 +08:00
cdir.write_to_stream(@output_stream)
end
protected
def finish
@compressor.finish
end
public
2014-03-01 06:31:10 +08:00
# Modeled after IO.<<
def <<(data)
@compressor << data
2014-03-01 06:31:10 +08:00
self
end
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.