rubyzip/zip.rb

1225 lines
30 KiB
Ruby
Executable File

#!/usr/bin/env ruby
require 'delegate'
require 'singleton'
require 'tempfile'
require 'ftools'
require 'zlib'
unless Enumerable.instance_methods(true).include?("inject")
module Enumerable #:nodoc:all
def inject(n = 0)
each { |value| n = yield(n, value) }
n
end
end
end
class String
def startsWith(aString)
slice(0, aString.size) == aString
end
def endsWith(aString)
aStringSize = aString.size
slice(-aStringSize, aStringSize) == aString
end
def ensureEnd(aString)
endsWith(aString) ? self : self + aString
end
end
class Time
#MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
#
# Register CX, the Time:
# Bits 0-4 2 second increments (0-29)
# Bits 5-10 minutes (0-59)
# bits 11-15 hours (0-24)
#
# Register DX, the Date:
# Bits 0-4 day (1-31)
# bits 5-8 month (1-12)
# bits 9-15 year (four digit year minus 1980)
def toBinaryDosDate
(sec/2) +
(min << 5) +
(hour << 11)
end
def toBinaryDosTime
(day) +
(month << 5) +
((year - 1980) << 9)
end
# Dos time is only stored with two seconds accuracy
def dosEquals(other)
(year == other.year &&
month == other.month &&
day == other.day &&
hour == other.hour &&
min == other.min &&
sec/2 == other.sec/2)
end
def self.parseBinaryDosFormat(binaryDosDate, binaryDosTime)
second = 2 * ( 0b11111 & binaryDosTime)
minute = ( 0b11111100000 & binaryDosTime) >> 5
hour = (0b1111100000000000 & binaryDosTime) >> 11
day = ( 0b11111 & binaryDosDate)
month = ( 0b111100000 & binaryDosDate) >> 5
year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980
begin
return Time.local(year, month, day, hour, minute, second)
end
end
end
module Zlib
if ! const_defined? :MAX_WBITS
MAX_WBITS = Zlib::Deflate.MAX_WBITS
end
end
module Zip
RUBY_MINOR_VERSION = VERSION.split(".")[1].to_i
# Ruby 1.7.x compatibility
# In ruby 1.6.x and 1.8.0 reading from an empty stream returns
# an empty string the first time and then nil.
# not so in 1.7.x
EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
module FakeIO
def kind_of?(object)
object == IO || super
end
end
# Implements many of the convenience methods of IO
# such as gets, getc, readline and readlines
# depends on: inputFinished?, produceInput and read
module AbstractInputStream
include Enumerable
include FakeIO
def initialize
super
@lineno = 0
@outputBuffer = ""
end
attr_accessor :lineno
def readlines(aSepString = $/)
retVal = []
each_line(aSepString) { |line| retVal << line }
return retVal
end
def gets(aSepString=$/)
@lineno = @lineno.next
return read if aSepString == nil
aSepString="#{$/}#{$/}" if aSepString == ""
bufferIndex=0
while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil)
bufferIndex=@outputBuffer.length
if inputFinished?
return @outputBuffer.empty? ? nil : flush
end
@outputBuffer << produceInput
end
sepIndex=matchIndex + aSepString.length
return @outputBuffer.slice!(0...sepIndex)
end
def flush
retVal=@outputBuffer
@outputBuffer=""
return retVal
end
def readline(aSepString = $/)
retVal = gets(aSepString)
raise EOFError if retVal == nil
return retVal
end
def each_line(aSepString = $/)
while true
yield readline(aSepString)
end
rescue EOFError
end
alias_method :each, :each_line
end
#relies on <<
module AbstractOutputStream
include FakeIO
def write(data)
self << data
data.to_s.length
end
def print(*params)
self << params.to_s << $\.to_s
end
def printf(aFormatString, *params)
self << sprintf(aFormatString, *params)
end
def putc(anObject)
self << case anObject
when Fixnum then anObject.chr
when String then anObject
else raise TypeError, "putc: Only Fixnum and String supported"
end
anObject
end
def puts(*params)
params << "\n" if params.empty?
params.flatten.each {
|element|
val = element.to_s
self << val
self << "\n" unless val[-1,1] == "\n"
}
end
end
class ZipInputStream
include AbstractInputStream
def initialize(filename, offset = 0)
super()
@archiveIO = File.open(filename, "rb")
@archiveIO.seek(offset, IO::SEEK_SET)
@decompressor = NullDecompressor.instance
@currentEntry = nil
end
def close
@archiveIO.close
end
def ZipInputStream.open(filename)
return new(filename) unless block_given?
zio = new(filename)
yield zio
ensure
zio.close if zio
end
def getNextEntry
@archiveIO.seek(@currentEntry.nextHeaderOffset,
IO::SEEK_SET) if @currentEntry
openEntry
end
def rewind
return if @currentEntry.nil?
@lineno = 0
@archiveIO.seek(@currentEntry.localHeaderOffset,
IO::SEEK_SET)
openEntry
end
def openEntry
@currentEntry = ZipEntry.readLocalEntry(@archiveIO)
if (@currentEntry == nil)
@decompressor = NullDecompressor.instance
elsif @currentEntry.compressionMethod == ZipEntry::STORED
@decompressor = PassThruDecompressor.new(@archiveIO,
@currentEntry.size)
elsif @currentEntry.compressionMethod == ZipEntry::DEFLATED
@decompressor = Inflater.new(@archiveIO)
else
raise ZipCompressionMethodError,
"Unsupported compression method #{@currentEntry.compressionMethod}"
end
flush
return @currentEntry
end
def read(numberOfBytes = nil)
@decompressor.read(numberOfBytes)
end
protected
def produceInput
@decompressor.produceInput
end
def inputFinished?
@decompressor.inputFinished?
end
end
class Decompressor #:nodoc:all
CHUNK_SIZE=32768
def initialize(inputStream)
super()
@inputStream=inputStream
end
end
class Inflater < Decompressor #:nodoc:all
def initialize(inputStream)
super
@zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
@outputBuffer=""
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
end
def read(numberOfBytes = nil)
readEverything = (numberOfBytes == nil)
while (readEverything || @outputBuffer.length < numberOfBytes)
break if internalInputFinished?
@outputBuffer << internalProduceInput
end
return valueWhenFinished if @outputBuffer.length==0 && inputFinished?
endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
return @outputBuffer.slice!(0...endIndex)
end
def produceInput
if (@outputBuffer.empty?)
return internalProduceInput
else
return @outputBuffer.slice!(0...(@outputBuffer.length))
end
end
# to be used with produceInput, not read (as read may still have more data cached)
def inputFinished?
@outputBuffer.empty? && internalInputFinished?
end
private
def internalProduceInput
@zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE))
end
def internalInputFinished?
@zlibInflater.finished?
end
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
def valueWhenFinished # mimic behaviour of ruby File object.
return nil if @hasReturnedEmptyString
@hasReturnedEmptyString=true
return ""
end
end
class PassThruDecompressor < Decompressor #:nodoc:all
def initialize(inputStream, charsToRead)
super inputStream
@charsToRead = charsToRead
@readSoFar = 0
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
end
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
def read(numberOfBytes = nil)
if inputFinished?
hasReturnedEmptyStringVal=@hasReturnedEmptyString
@hasReturnedEmptyString=true
return "" unless hasReturnedEmptyStringVal
return nil
end
if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
numberOfBytes = @charsToRead-@readSoFar
end
@readSoFar += numberOfBytes
@inputStream.read(numberOfBytes)
end
def produceInput
read(Decompressor::CHUNK_SIZE)
end
def inputFinished?
(@readSoFar >= @charsToRead)
end
end
class NullDecompressor #:nodoc:all
include Singleton
def read(numberOfBytes = nil)
nil
end
def produceInput
nil
end
def inputFinished?
true
end
end
class NullInputStream < NullDecompressor #:nodoc:all
include AbstractInputStream
end
class ZipEntry
STORED = 0
DEFLATED = 8
attr_accessor :comment, :compressedSize, :crc, :extra, :compressionMethod,
:name, :size, :localHeaderOffset, :time
alias :mtime :time
def initialize(zipfile = "", name = "", comment = "", extra = "",
compressedSize = 0, crc = 0,
compressionMethod = ZipEntry::DEFLATED, size = 0,
time = Time.now)
super()
if name.startsWith("/")
raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
end
@localHeaderOffset = 0
@zipfile, @comment, @compressedSize, @crc, @extra, @compressionMethod,
@name, @size = zipfile, comment, compressedSize, crc,
extra, compressionMethod, name, size
@time = time
end
def directory?
return (%r{\/$} =~ @name) != nil
end
alias :isDirectory :directory?
def file?
! directory?
end
def localEntryOffset #:nodoc:all
localHeaderOffset + localHeaderSize
end
def localHeaderSize #:nodoc:all
LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.size : 0)
end
def cdirHeaderSize #:nodoc:all
CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
(@extra ? @extra.size : 0) + (@comment ? @comment.size : 0)
end
def nextHeaderOffset #:nodoc:all
localEntryOffset + self.compressedSize
end
def to_s
@name
end
protected
def ZipEntry.readZipShort(io)
io.read(2).unpack('v')[0]
end
def ZipEntry.readZipLong(io)
io.read(4).unpack('V')[0]
end
public
LOCAL_ENTRY_SIGNATURE = 0x04034b50
LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
def readLocalEntry(io) #:nodoc:all
@localHeaderOffset = io.tell
staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH)
unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH)
raise ZipError, "Premature end of file. Not enough data for zip entry local header"
end
localHeader ,
@version ,
@gpFlags ,
@compressionMethod,
lastModTime ,
lastModDate ,
@crc ,
@compressedSize ,
@size ,
nameLength ,
extraLength = staticSizedFieldsBuf.unpack('VvvvvvVVVvv')
unless (localHeader == LOCAL_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
end
setTime(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
unless (@extra && @extra.length == extraLength)
raise ZipError, "Truncated local zip entry header"
end
end
def ZipEntry.readLocalEntry(io)
entry = new(io.path)
entry.readLocalEntry(io)
return entry
rescue ZipError
return nil
end
def writeLocalEntry(io) #:nodoc:all
@localHeaderOffset = io.tell
io <<
[LOCAL_ENTRY_SIGNATURE ,
0 , # @version ,
0 , # @gpFlags ,
@compressionMethod ,
@time.toBinaryDosDate , # @lastModTime ,
@time.toBinaryDosTime , # @lastModDate ,
@crc ,
@compressedSize ,
@size ,
@name ? @name.length : 0,
@extra? @extra.length : 0 ].pack('VvvvvvVVVvv')
io << @name
io << @extra
end
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
def readCDirEntry(io) #:nodoc:all
staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH)
unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH)
raise ZipError, "Premature end of file. Not enough data for zip cdir entry header"
end
cdirSignature ,
@version ,
@versionNeededToExtract,
@gpFlags ,
@compressionMethod ,
lastModTime ,
lastModDate ,
@crc ,
@compressedSize ,
@size ,
nameLength ,
extraLength ,
commentLength ,
diskNumberStart ,
@internalFileAttributes,
@externalFileAttributes,
@localHeaderOffset ,
@name ,
@extra ,
@comment = staticSizedFieldsBuf.unpack('VvvvvvvVVVvvvvvVV')
unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
end
setTime(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
@comment = io.read(commentLength)
unless (@comment && @comment.length == commentLength)
raise ZipError, "Truncated cdir zip entry header"
end
end
def ZipEntry.readCDirEntry(io) #:nodoc:all
entry = new(io.path)
entry.readCDirEntry(io)
return entry
rescue ZipError
return nil
end
def writeCDirEntry(io) #:nodoc:all
io <<
[CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
0 , # @version ,
0 , # @versionNeededToExtract ,
0 , # @gpFlags ,
@compressionMethod ,
@time.toBinaryDosDate , # @lastModTime ,
@time.toBinaryDosTime , # @lastModDate ,
@crc ,
@compressedSize ,
@size ,
@name ? @name.length : 0 ,
@extra ? @extra.length : 0 ,
@comment ? comment.length : 0 ,
0 , # disk number start
0 , # @internalFileAttributes ,
0 , # @externalFileAttributes ,
@localHeaderOffset ,
@name ,
@extra ,
@comment ].pack('VvvvvvvVVVvvvvvVV')
io << @name
io << @extra
io << @comment
end
def == (other)
return false unless other.class == ZipEntry
# Compares contents of local entry and exposed fields
(@compressionMethod == other.compressionMethod &&
@crc == other.crc &&
@compressedSize == other.compressedSize &&
@size == other.size &&
@name == other.name &&
@extra == other.extra &&
@time.dosEquals(other.time))
end
def <=> (other)
return to_s <=> other.to_s
end
def getInputStream
zis = ZipInputStream.new(@zipfile, localHeaderOffset)
zis.getNextEntry
if block_given?
begin
return yield(zis)
ensure
zis.close
end
else
return zis
end
end
def writeToZipOutputStream(aZipOutputStream) #:nodoc:all
aZipOutputStream.putNextEntry(self.dup)
aZipOutputStream << getRawInputStream {
|is|
is.seek(localEntryOffset, IO::SEEK_SET)
is.read(compressedSize)
}
end
def parentAsString
val = name[/.*(?=[^\/](\/)?)/]
val == "" ? nil : val
end
private
def getRawInputStream(&aProc)
File.open(@zipfile, "rb", &aProc)
end
def setTime(binaryDosDate, binaryDosTime)
@time = Time.parseBinaryDosFormat(binaryDosDate, binaryDosTime)
rescue ArgumentError
puts "Invalid date/time in zip entry"
end
end
class ZipOutputStream
include AbstractOutputStream
attr_accessor :comment
def initialize(fileName)
super()
@fileName = fileName
@outputStream = File.new(@fileName, "wb")
@entrySet = ZipEntrySet.new
@compressor = NullCompressor.instance
@closed = false
@currentEntry = nil
@comment = nil
end
def ZipOutputStream.open(fileName)
return new(fileName) unless block_given?
zos = new(fileName)
yield zos
ensure
zos.close if zos
end
def close
return if @closed
finalizeCurrentEntry
updateLocalHeaders
writeCentralDirectory
@outputStream.close
@closed = true
end
def putNextEntry(entry, level = Zlib::DEFAULT_COMPRESSION)
raise ZipError, "zip stream is closed" if @closed
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s)
initNextEntry(newEntry)
@currentEntry=newEntry
end
private
def finalizeCurrentEntry
return unless @currentEntry
finish
@currentEntry.compressedSize = @outputStream.tell - @currentEntry.localHeaderOffset -
@currentEntry.localHeaderSize
@currentEntry.size = @compressor.size
@currentEntry.crc = @compressor.crc
@currentEntry = nil
@compressor = NullCompressor.instance
end
def initNextEntry(entry, level = Zlib::DEFAULT_COMPRESSION)
finalizeCurrentEntry
@entrySet << entry
entry.writeLocalEntry(@outputStream)
@compressor = getCompressor(entry, level)
end
def getCompressor(entry, level)
case entry.compressionMethod
when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
else raise ZipCompressionMethodError,
"Invalid compression method: '#{entry.compressionMethod}'"
end
end
def updateLocalHeaders
pos = @outputStream.tell
@entrySet.each {
|entry|
@outputStream.pos = entry.localHeaderOffset
entry.writeLocalEntry(@outputStream)
}
@outputStream.pos = pos
end
def writeCentralDirectory
cdir = ZipCentralDirectory.new(@entrySet, @comment)
cdir.writeToStream(@outputStream)
end
protected
def finish
@compressor.finish
end
public
def << (data)
@compressor << data
end
end
class Compressor #:nodoc:all
def finish
end
end
class PassThruCompressor < Compressor #:nodoc:all
def initialize(outputStream)
super()
@outputStream = outputStream
@crc = Zlib::crc32
@size = 0
end
def << (data)
val = data.to_s
@crc = Zlib::crc32(val, @crc)
@size += val.size
@outputStream << val
end
attr_reader :size, :crc
end
class NullCompressor < Compressor #:nodoc:all
include Singleton
def << (data)
raise IOError, "closed stream"
end
attr_reader :size, :compressedSize
end
class Deflater < Compressor #:nodoc:all
def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
super()
@outputStream = outputStream
@zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
@size = 0
@crc = Zlib::crc32
end
def << (data)
val = data.to_s
@crc = Zlib::crc32(val, @crc)
@size += val.size
@outputStream << @zlibDeflater.deflate(data)
end
def finish
until @zlibDeflater.finished?
@outputStream << @zlibDeflater.finish
end
end
attr_reader :size, :crc
end
class ZipEntrySet
include Enumerable
def initialize(anEnumerable = [])
super()
@entrySet = {}
anEnumerable.each { |o| push(o) }
end
def include?(entry)
@entrySet.include?(entry.to_s)
end
def <<(entry)
@entrySet[entry.to_s] = entry
end
alias :push :<<
def size
@entrySet.size
end
alias :length :size
def delete(entry)
@entrySet.delete(entry.to_s) ? entry : nil
end
def each(&aProc)
@entrySet.values.each(&aProc)
end
def entries
@entrySet.values
end
# deep clone
def dup
newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup })
end
def == (other)
return false unless other.kind_of?(ZipEntrySet)
return @entrySet == other.entrySet
end
def parent(entry)
@entrySet[entry.parentAsString]
end
#TODO attr_accessor :autoCreateDirectories
protected
attr_accessor :entrySet
end
class ZipCentralDirectory #:nodoc:all
include Enumerable
END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
STATIC_EOCD_SIZE = 22
attr_reader :size, :comment
def entries
@entrySet.entries
end
def initialize(entries = ZipEntrySet.new, comment = "")
super()
@entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
@comment = comment
end
def writeToStream(io)
offset = io.tell
@entrySet.each { |entry| entry.writeCDirEntry(io) }
writeEOCD(io, offset)
end
def writeEOCD(io, offset)
io <<
[END_OF_CENTRAL_DIRECTORY_SIGNATURE,
0 , # @numberOfThisDisk
0 , # @numberOfDiskWithStartOfCDir
@entrySet? @entrySet.size : 0 ,
@entrySet? @entrySet.size : 0 ,
cdirSize ,
offset ,
@comment ? @comment.length : 0 ].pack('VvvvvVVv')
io << @comment
end
private :writeEOCD
def cdirSize
# does not include eocd
@entrySet.inject(0) { |value, entry| entry.cdirHeaderSize + value }
end
private :cdirSize
def readEOCD(io)
buf = getEOCD(io)
@numberOfThisDisk = ZipEntry::readZipShort(buf)
@numberOfDiskWithStartOfCDir = ZipEntry::readZipShort(buf)
@totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::readZipShort(buf)
@size = ZipEntry::readZipShort(buf)
@sizeInBytes = ZipEntry::readZipLong(buf)
@cdirOffset = ZipEntry::readZipLong(buf)
commentLength = ZipEntry::readZipShort(buf)
@comment = buf.read(commentLength)
raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
end
def readCentralDirectoryEntries(io)
begin
io.seek(@cdirOffset, IO::SEEK_SET)
rescue Errno::EINVAL
raise ZipError, "Zip consistency problem while reading central directory entry"
end
@entrySet = ZipEntrySet.new
@size.times {
@entrySet << ZipEntry.readCDirEntry(io)
}
end
def readFromStream(io)
readEOCD(io)
readCentralDirectoryEntries(io)
end
def getEOCD(io)
begin
io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
rescue Errno::EINVAL
io.seek(0, IO::SEEK_SET)
end
buf = io.read
sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V'))
raise ZipError, "Zip end of central directory signature not found" unless sigIndex
buf=buf.slice!((sigIndex+4)...(buf.size))
def buf.read(count)
slice!(0, count)
end
return buf
end
def each(&proc)
@entrySet.each(&proc)
end
def ZipCentralDirectory.readFromStream(io)
cdir = new
cdir.readFromStream(io)
return cdir
rescue ZipError
return nil
end
def == (other)
return false unless other.kind_of?(ZipCentralDirectory)
@entrySet.entries.sort == other.entries.sort && comment == other.comment
end
end
class ZipError < StandardError ; end
class ZipEntryExistsError < ZipError; end
class ZipDestinationFileExistsError < ZipError; end
class ZipCompressionMethodError < ZipError; end
class ZipEntryNameError < ZipError; end
class ZipFile < ZipCentralDirectory
CREATE = 1
attr_reader :name
def initialize(fileName, create = nil)
super()
@name = fileName
@comment = ""
if (File.exists?(fileName))
File.open(name, "rb") { |f| readFromStream(f) }
elsif (create == ZipFile::CREATE)
@entrySet = ZipEntrySet.new
else
raise ZipError, "File #{fileName} not found"
end
@create = create
@storedEntries = @entrySet.dup
end
def ZipFile.open(fileName, create = nil)
zf = ZipFile.new(fileName, create)
if block_given?
begin
yield zf
ensure
zf.close
end
else
zf
end
end
attr_accessor :comment
def ZipFile.foreach(aZipFileName, &block)
ZipFile.open(aZipFileName) {
|zipFile|
zipFile.each(&block)
}
end
def getInputStream(entry, &aProc)
getEntry(entry).getInputStream(&aProc)
end
def to_s
@name
end
def add(entry, srcPath, &continueOnExistsProc)
continueOnExistsProc ||= proc { false }
checkEntryExists(entry, continueOnExistsProc, "add")
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
if isDirectory(newEntry, srcPath)
@entrySet << ZipStreamableDirectory.new(newEntry)
else
@entrySet << ZipStreamableFile.new(newEntry, srcPath)
end
end
def remove(entry)
@entrySet.delete(getEntry(entry))
end
def rename(entry, newName, &continueOnExistsProc)
foundEntry = getEntry(entry)
checkEntryExists(newName, continueOnExistsProc, "rename")
foundEntry.name=newName
end
def replace(entry, srcPath)
checkFile(srcPath)
add(remove(entry), srcPath)
end
def extract(entry, destPath, &onExistsProc)
onExistsProc ||= proc { false }
foundEntry = getEntry(entry)
if foundEntry.isDirectory
createDirectory(foundEntry, destPath, &onExistsProc)
else
writeFile(foundEntry, destPath, &onExistsProc)
end
end
def commit
return if ! commitRequired?
onSuccessReplace(name) {
|tmpFile|
ZipOutputStream.open(tmpFile) {
|zos|
@entrySet.each { |e| e.writeToZipOutputStream(zos) }
zos.comment = comment
}
true
}
initialize(name)
end
def close
commit
end
def commitRequired?
return @entrySet != @storedEntries || @create == ZipFile::CREATE
end
def findEntry(entry)
@entrySet.detect {
|e|
e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
}
end
def getEntry(entry)
selectedEntry = findEntry(entry)
unless selectedEntry
raise Errno::ENOENT,
"No matching entry found in zip file '#{@name}' for '#{entry}'"
end
return selectedEntry
end
private
def createDirectory(entry, destPath)
if File.directory? destPath
return
elsif File.exists? destPath
if block_given? && yield(entry, destPath)
File.rm_f destPath
else
raise ZipDestinationFileExistsError,
"Cannot create directory '#{destPath}'. "+
"A file already exists with that name"
end
end
Dir.mkdir destPath
end
def isDirectory(newEntry, srcPath)
srcPathIsDirectory = File.directory?(srcPath)
if newEntry.isDirectory && ! srcPathIsDirectory
raise ArgumentError,
"entry name '#{newEntry}' indicates directory entry, but "+
"'#{srcPath}' is not a directory"
elsif ! newEntry.isDirectory && srcPathIsDirectory
newEntry.name += "/"
end
return newEntry.isDirectory && srcPathIsDirectory
end
def checkEntryExists(entryName, continueOnExistsProc, procedureName)
continueOnExistsProc ||= proc { false }
if @entrySet.detect { |e| e.name == entryName }
if continueOnExistsProc.call
remove getEntry(entryName)
else
raise ZipEntryExistsError,
procedureName+" failed. Entry #{entryName} already exists"
end
end
end
def writeFile(entry, destPath, continueOnExistsProc = proc { false })
if File.exists?(destPath) && ! yield(entry, destPath)
raise ZipDestinationFileExistsError,
"Destination '#{destPath}' already exists"
end
File.open(destPath, "wb") {
|os|
entry.getInputStream { |is| os << is.read }
}
end
def checkFile(path)
unless File.readable? path
raise Errno::ENOENT,
"'#{path}' does not exist or cannot be opened reading"
end
end
def onSuccessReplace(aFilename)
tmpfile = getTempfile
tmpFilename = tmpfile.path
tmpfile.close
if yield tmpFilename
File.move(tmpFilename, name)
end
end
def getTempfile
Tempfile.new(File.basename(name), File.dirname(name)).binmode
end
end
class ZipStreamableFile < DelegateClass(ZipEntry) #:nodoc:all
def initialize(entry, filepath)
super(entry)
@delegate = entry
@filepath = filepath
end
def getInputStream(&aProc)
File.open(@filepath, "rb", &aProc)
end
def writeToZipOutputStream(aZipOutputStream)
aZipOutputStream.putNextEntry(self)
aZipOutputStream << getInputStream { |is| is.read }
end
def == (other)
return false unless other.class == ZipStreamableFile
@filepath == other.filepath && super(other.delegate)
end
protected
attr_reader :filepath, :delegate
end
class ZipStreamableDirectory < DelegateClass(ZipEntry) #:nodoc:all
def initialize(entry)
super(entry)
end
def getInputStream(&aProc)
return yield(NullInputStream.instance) if block_given?
NullInputStream.instance
end
def writeToZipOutputStream(aZipOutputStream)
aZipOutputStream.putNextEntry(self)
end
end
end # Zip namespace module
# Copyright (C) 2002 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or
# modify it under the terms of the ruby license.