archive-zip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CONTRIBUTORS +13 -0
- data/GPL +676 -0
- data/HACKING +122 -0
- data/LEGAL +8 -0
- data/LICENSE +57 -0
- data/MANIFEST +26 -0
- data/NEWS +22 -0
- data/README +130 -0
- data/lib/archive/support/io-like.rb +12 -0
- data/lib/archive/support/io.rb +14 -0
- data/lib/archive/support/iowindow.rb +123 -0
- data/lib/archive/support/stringio.rb +22 -0
- data/lib/archive/support/time.rb +85 -0
- data/lib/archive/support/zlib.rb +211 -0
- data/lib/archive/zip.rb +643 -0
- data/lib/archive/zip/codec.rb +30 -0
- data/lib/archive/zip/codec/deflate.rb +206 -0
- data/lib/archive/zip/codec/store.rb +241 -0
- data/lib/archive/zip/datadescriptor.rb +54 -0
- data/lib/archive/zip/entry.rb +991 -0
- data/lib/archive/zip/error.rb +22 -0
- data/lib/archive/zip/extrafield.rb +23 -0
- data/lib/archive/zip/extrafield/extendedtimestamp.rb +101 -0
- data/lib/archive/zip/extrafield/raw.rb +32 -0
- data/lib/archive/zip/extrafield/unix.rb +101 -0
- data/test/test_archive.rb +8 -0
- metadata +98 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class StringIO
|
4
|
+
unless StringIO.method_defined?(:readbytes)
|
5
|
+
# Copied from IO#readbytes.
|
6
|
+
def readbytes(n)
|
7
|
+
str = read(n)
|
8
|
+
if str == nil
|
9
|
+
raise EOFError, "end of file reached"
|
10
|
+
end
|
11
|
+
if str.size < n
|
12
|
+
raise TruncatedDataError.new("data truncated", str)
|
13
|
+
end
|
14
|
+
str
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Always returns +true+. Added for compatibility with IO#seekable?.
|
19
|
+
def seekable?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Time
|
2
|
+
# Returns a DOSTime representing this time object as a DOS date-time
|
3
|
+
# structure. Times are bracketed by the limits of the ability of the DOS
|
4
|
+
# date-time structure to represent them. Accuracy is 2 seconds and years
|
5
|
+
# range from 1980 to 2099. The returned structure represents as closely as
|
6
|
+
# possible the time of this object.
|
7
|
+
#
|
8
|
+
# See DOSTime#new for a description of this structure.
|
9
|
+
def to_dos_time
|
10
|
+
dos_sec = sec/2
|
11
|
+
dos_year = year - 1980
|
12
|
+
dos_year = 0 if dos_year < 0
|
13
|
+
dos_year = 119 if dos_year > 119
|
14
|
+
|
15
|
+
DOSTime.new(
|
16
|
+
(dos_sec ) |
|
17
|
+
(min << 5) |
|
18
|
+
(hour << 11) |
|
19
|
+
(day << 16) |
|
20
|
+
(month << 21) |
|
21
|
+
(dos_year << 25)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# A representation of the DOS time structure which can be converted into
|
27
|
+
# instances of Time.
|
28
|
+
class DOSTime
|
29
|
+
include Comparable
|
30
|
+
|
31
|
+
# Creates a new instance of DOSTime. _dos_time_ is a 4 byte String or
|
32
|
+
# unsigned number (Integer) representing an MS-DOS time structure where:
|
33
|
+
# Bits 0-4:: 2 second increments (0-29)
|
34
|
+
# Bits 5-10:: minutes (0-59)
|
35
|
+
# Bits 11-15:: hours (0-24)
|
36
|
+
# Bits 16-20:: day (1-31)
|
37
|
+
# Bits 21-24:: month (1-12)
|
38
|
+
# Bits 25-31:: four digit year minus 1980 (0-119)
|
39
|
+
#
|
40
|
+
# If _dos_time_ is ommitted or +nil+, a new instance is created based on the
|
41
|
+
# current time.
|
42
|
+
def initialize(dos_time = nil)
|
43
|
+
case dos_time
|
44
|
+
when nil
|
45
|
+
@dos_time = Time.now.to_dos_time.dos_time
|
46
|
+
when Integer
|
47
|
+
@dos_time = dos_time
|
48
|
+
else
|
49
|
+
unless dos_time.length == 4 then
|
50
|
+
raise ArgumentError, 'length of DOS time structure is not 4'
|
51
|
+
end
|
52
|
+
@dos_time = dos_time.unpack('V')[0]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns -1 if _other_ is a time earlier than this one, 0 if _other_ is the
|
57
|
+
# same time, and 1 if _other_ is a later time.
|
58
|
+
def cmp(other)
|
59
|
+
@dos_time <=> other.dos_time
|
60
|
+
end
|
61
|
+
alias :<=> :cmp
|
62
|
+
|
63
|
+
# Returns the time value of this object as an integer representing the DOS
|
64
|
+
# time structure.
|
65
|
+
def to_i
|
66
|
+
@dos_time
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a Time instance which is equivalent to the time represented by this
|
70
|
+
# object.
|
71
|
+
def to_time
|
72
|
+
second = ((0b11111 & @dos_time) ) * 2
|
73
|
+
minute = ((0b111111 << 5 & @dos_time) >> 5)
|
74
|
+
hour = ((0b11111 << 11 & @dos_time) >> 11)
|
75
|
+
day = ((0b11111 << 16 & @dos_time) >> 16)
|
76
|
+
month = ((0b1111 << 21 & @dos_time) >> 21)
|
77
|
+
year = ((0b1111111 << 25 & @dos_time) >> 25) + 1980
|
78
|
+
return Time.local(year, month, day, hour, minute, second)
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
# Used by _cmp_ to read another time stored in another DOSTime instance.
|
84
|
+
attr_reader :dos_time # :nodoc:
|
85
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
require 'archive/support/io-like'
|
4
|
+
|
5
|
+
module Zlib # :nodoc:
|
6
|
+
if ! const_defined?(:MAX_WBITS) then
|
7
|
+
MAX_WBITS = Deflate::MAX_WBITS
|
8
|
+
end
|
9
|
+
|
10
|
+
# Zlib::ZWriter is a writable, IO-like object (includes IO::Like) which wraps
|
11
|
+
# other writable, IO-like objects in order to facilitate writing data to those
|
12
|
+
# objects using the deflate method of compression.
|
13
|
+
class ZWriter
|
14
|
+
include IO::Like
|
15
|
+
|
16
|
+
# Creates a new instance of this class with the given arguments using #new
|
17
|
+
# and then passes the instance to the given block. The #close method is
|
18
|
+
# guaranteed to be called after the block completes.
|
19
|
+
#
|
20
|
+
# Equivalent to #new if no block is given.
|
21
|
+
def self.open(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
|
22
|
+
zw = new(io, level, window_bits, mem_level, strategy)
|
23
|
+
return zw unless block_given?
|
24
|
+
|
25
|
+
begin
|
26
|
+
yield(zw)
|
27
|
+
ensure
|
28
|
+
zw.close unless zw.closed?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a new instance of this class. _io_ must respond to the _write_
|
33
|
+
# method as an instance of IO would. _level_, _window_bits_, _mem_level_,
|
34
|
+
# and _strategy_ are all passed directly to Zlib::Deflate.new(). See the
|
35
|
+
# documentation of that method for their meanings.
|
36
|
+
#
|
37
|
+
# NOTE: Due to limitations in Ruby's finalization capabilities, the #close
|
38
|
+
# method is _not_ automatically called when this object is garbage
|
39
|
+
# collected. Make sure to call #close when finished with this object.
|
40
|
+
def initialize(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
|
41
|
+
@delegate = io
|
42
|
+
@deflater = Zlib::Deflate.new(level, window_bits, mem_level, strategy)
|
43
|
+
@deflate_buffer = ''
|
44
|
+
@crc32 = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# The CRC32 checksum of the uncompressed data written using this object.
|
48
|
+
#
|
49
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
50
|
+
# so calling #flush prior to examining this attribute may be necessary for
|
51
|
+
# an accurate computation.
|
52
|
+
attr_reader :crc32
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
# The delegate object to which compressed data is written.
|
57
|
+
attr_reader :delegate
|
58
|
+
|
59
|
+
public
|
60
|
+
|
61
|
+
# Closes the writer by finishing the compressed data and flushing it to the
|
62
|
+
# delegate.
|
63
|
+
#
|
64
|
+
# Raises IOError if called more than once.
|
65
|
+
def close
|
66
|
+
super()
|
67
|
+
delegate.write(@deflater.finish)
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the number of bytes of compressed data produced so far.
|
72
|
+
#
|
73
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
74
|
+
# so calling #flush prior to calling this method may be necessary for an
|
75
|
+
# accurate count.
|
76
|
+
def compressed_size
|
77
|
+
@deflater.total_out
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the number of bytes sent to be compressed so far.
|
81
|
+
#
|
82
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
83
|
+
# so calling #flush prior to calling this method may be necessary for an
|
84
|
+
# accurate count.
|
85
|
+
def uncompressed_size
|
86
|
+
@deflater.total_in
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def unbuffered_write(string)
|
92
|
+
until @deflate_buffer.empty? do
|
93
|
+
@deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
|
94
|
+
end
|
95
|
+
@deflate_buffer = @deflater.deflate(string)
|
96
|
+
|
97
|
+
begin
|
98
|
+
@deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
|
99
|
+
rescue Errno::EINTR, Errno::EAGAIN
|
100
|
+
# Ignore this because everything is in the deflate buffer and will be
|
101
|
+
# attempted again the next time this method is called.
|
102
|
+
end
|
103
|
+
@crc32 = Zlib.crc32(string, @crc32)
|
104
|
+
string.length
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Zlib::ZReader is a readable, IO-like object (includes IO::Like) which wraps
|
109
|
+
# other readable, IO-like objects in order to facilitate reading data from
|
110
|
+
# those objects using the inflate method of decompression.
|
111
|
+
class ZReader
|
112
|
+
include IO::Like
|
113
|
+
|
114
|
+
# Creates a new instance of this class with the given arguments using #new
|
115
|
+
# and then passes the instance to the given block. The #close method is
|
116
|
+
# guaranteed to be called after the block completes.
|
117
|
+
#
|
118
|
+
# Equivalent to #new if no block is given.
|
119
|
+
def self.open(io, window_bits = nil)
|
120
|
+
zr = new(io, window_bits)
|
121
|
+
return zr unless block_given?
|
122
|
+
|
123
|
+
begin
|
124
|
+
yield(zr)
|
125
|
+
ensure
|
126
|
+
zr.close unless zr.closed?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Creates a new instance of this class. _io_ must respond to the _read_
|
131
|
+
# method as an IO instance would. _window_bits_ is passed directly to
|
132
|
+
# Zlib::Inflate.new(). See the documentation of that method for its
|
133
|
+
# meaning. If _io_ also responds to _rewind_, then the _rewind_ method of
|
134
|
+
# this class can be used to reset the whole stream back to the beginning.
|
135
|
+
#
|
136
|
+
# NOTE: Due to limitations in Ruby's finalization capabilities, the #close
|
137
|
+
# method is _not_ automatically called when this object is garbage
|
138
|
+
# collected. Make sure to call #close when finished with this object.
|
139
|
+
def initialize(io, window_bits = nil)
|
140
|
+
@delegate = io
|
141
|
+
@window_bits = window_bits
|
142
|
+
@inflater = Zlib::Inflate.new(@window_bits)
|
143
|
+
@crc32 = 0
|
144
|
+
@decompress_buffer = ''
|
145
|
+
end
|
146
|
+
|
147
|
+
# The CRC32 checksum of the uncompressed data read using this object.
|
148
|
+
#
|
149
|
+
# NOTE: The contents of the internal read buffer are immediately processed
|
150
|
+
# any time the buffer is filled, so this count is only accurate if all data
|
151
|
+
# has been read out of this object.
|
152
|
+
attr_reader :crc32
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
# The delegate object from which compressed data is read.
|
157
|
+
attr_reader :delegate
|
158
|
+
|
159
|
+
public
|
160
|
+
|
161
|
+
# Closes the reader.
|
162
|
+
#
|
163
|
+
# Raises IOError if called more than once.
|
164
|
+
def close
|
165
|
+
super()
|
166
|
+
@inflater.close
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the number of bytes sent to be decompressed so far.
|
171
|
+
def compressed_size
|
172
|
+
@inflater.total_in
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the number of bytes of decompressed data produced so far.
|
176
|
+
def uncompressed_size
|
177
|
+
@inflater.total_out
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def unbuffered_read(length)
|
183
|
+
raise EOFError, 'end of file reached' if @inflater.finished?
|
184
|
+
|
185
|
+
begin
|
186
|
+
while @decompress_buffer.length < length && ! @inflater.finished? do
|
187
|
+
@decompress_buffer << @inflater.inflate(delegate.read(1))
|
188
|
+
end
|
189
|
+
rescue Errno::EINTR, Errno::EAGAIN
|
190
|
+
raise if @decompress_buffer.empty?
|
191
|
+
end
|
192
|
+
buffer = @decompress_buffer.slice!(0, length)
|
193
|
+
@crc32 = Zlib.crc32(buffer, @crc32)
|
194
|
+
buffer
|
195
|
+
end
|
196
|
+
|
197
|
+
def unbuffered_seek(offset, whence = IO::SEEK_SET)
|
198
|
+
unless offset == 0 && whence == IO::SEEK_SET then
|
199
|
+
raise Errno::EINVAL, 'Invalid argument'
|
200
|
+
end
|
201
|
+
unless delegate.respond_to?(:rewind) then
|
202
|
+
raise Errno::ESPIPE, 'Illegal seek'
|
203
|
+
end
|
204
|
+
delegate.rewind
|
205
|
+
@inflater.close
|
206
|
+
@inflater = Zlib::Inflate.new(@window_bits)
|
207
|
+
@crc32 = 0
|
208
|
+
@decompress_buffer = ''
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
data/lib/archive/zip.rb
ADDED
@@ -0,0 +1,643 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'set'
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
require 'archive/support/io'
|
8
|
+
require 'archive/support/iowindow'
|
9
|
+
require 'archive/support/stringio'
|
10
|
+
require 'archive/support/time'
|
11
|
+
require 'archive/support/zlib'
|
12
|
+
require 'archive/zip/codec'
|
13
|
+
require 'archive/zip/entry'
|
14
|
+
require 'archive/zip/error'
|
15
|
+
|
16
|
+
module Archive # :nodoc:
|
17
|
+
# Archive::Zip represents a ZIP archive compatible with InfoZip tools and the
|
18
|
+
# archives they generate. It currently supports both stored and deflated ZIP
|
19
|
+
# entries, directory entries, file entries, and symlink entries. File and
|
20
|
+
# directory accessed and modified times, POSIX permissions, and ownerships can
|
21
|
+
# be archived and restored as well depending on platform support for such
|
22
|
+
# metadata.
|
23
|
+
#
|
24
|
+
# Zip64, digital signatures, and encryption are not supported. ZIP archives
|
25
|
+
# can only be read from seekable kinds of IO, such as files; reading archives
|
26
|
+
# from pipes or any other non-seekable kind of IO is not supported. However,
|
27
|
+
# writing to such IO objects <b><em>IS</em></b> supported.
|
28
|
+
class Zip
|
29
|
+
include Enumerable
|
30
|
+
|
31
|
+
# The lead-in marker for the end of central directory record.
|
32
|
+
EOCD_SIGNATURE = "PK\x5\x6" # 0x06054b50
|
33
|
+
# The lead-in marker for the digital signature record.
|
34
|
+
DS_SIGNATURE = "PK\x5\x5" # 0x05054b50
|
35
|
+
# The lead-in marker for the ZIP64 end of central directory record.
|
36
|
+
Z64EOCD_SIGNATURE = "PK\x6\x6" # 0x06064b50
|
37
|
+
# The lead-in marker for the ZIP64 end of central directory locator record.
|
38
|
+
Z64EOCDL_SIGNATURE = "PK\x6\x7" # 0x07064b50
|
39
|
+
# The lead-in marker for a central file record.
|
40
|
+
CFH_SIGNATURE = "PK\x1\x2" # 0x02014b50
|
41
|
+
# The lead-in marker for a local file record.
|
42
|
+
LFH_SIGNATURE = "PK\x3\x4" # 0x04034b50
|
43
|
+
# The lead-in marker for data descriptor record.
|
44
|
+
DD_SIGNATURE = "PK\x7\x8" # 0x08074b50
|
45
|
+
|
46
|
+
|
47
|
+
# A convenience method which opens a new or existing archive located in the
|
48
|
+
# path indicated by _archive_path_, adds and updates entries based on the
|
49
|
+
# paths given in _paths_, and then saves and closes the archive. See the
|
50
|
+
# instance method #archive for more information about _paths_ and _options_.
|
51
|
+
def self.archive(archive_path, paths, options = {})
|
52
|
+
open(archive_path) { |z| z.archive(paths, options) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# A convenience method which opens an archive located in the path indicated
|
56
|
+
# by _archive_path_, extracts the entries to the path indicated by
|
57
|
+
# _destination_, and then closes the archive. See the instance method
|
58
|
+
# #extract for more information about _destination_ and _options_.
|
59
|
+
def self.extract(archive_path, destination, options = {})
|
60
|
+
open(archive_path) { |z| z.extract(destination, options) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Calls #new with the given arguments and yields the resulting Zip instance
|
64
|
+
# to the given block. Returns the result of the block and ensures that the
|
65
|
+
# Zip instance is closed.
|
66
|
+
#
|
67
|
+
# This is a synonym for #new if no block is given.
|
68
|
+
def self.open(archive_path, archive_out = nil)
|
69
|
+
zf = new(archive_path, archive_out)
|
70
|
+
return zf unless block_given?
|
71
|
+
|
72
|
+
begin
|
73
|
+
yield(zf)
|
74
|
+
ensure
|
75
|
+
zf.close unless zf.closed?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Open and parse the file located at the path indicated by _archive_path_ if
|
80
|
+
# _archive_path_ is not +nil+ and the path exists. If _archive_out_ is
|
81
|
+
# unspecified or +nil+, any changes made will be saved in place, replacing
|
82
|
+
# the current archive with a new one having the same name. If _archive_out_
|
83
|
+
# is a String, it points to a file which will recieve the new archive's
|
84
|
+
# contents. Otherwise, _archive_out_ is assumed to be a writable, IO-like
|
85
|
+
# object operating in *binary* mode which will recieve the new archive's
|
86
|
+
# contents.
|
87
|
+
#
|
88
|
+
# At least one of _archive_path_ and _archive_out_ must be specified and
|
89
|
+
# non-nil; otherwise, an error will be raised.
|
90
|
+
def initialize(archive_path, archive_out = nil)
|
91
|
+
if (archive_path.nil? || archive_path.empty?) &&
|
92
|
+
(archive_out.nil? ||
|
93
|
+
archive_out.kind_of?(String) && archive_out.empty?) then
|
94
|
+
raise ArgumentError, 'No valid source or destination archive specified'
|
95
|
+
end
|
96
|
+
@archive_path = archive_path
|
97
|
+
@archive_out = archive_out
|
98
|
+
@entries = {}
|
99
|
+
@dirty = false
|
100
|
+
@comment = ''
|
101
|
+
@closed = false
|
102
|
+
if ! @archive_path.nil? && File.exist?(@archive_path) then
|
103
|
+
@archive_in = File.new(@archive_path, 'rb')
|
104
|
+
parse(@archive_in)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# A comment string for the ZIP archive.
|
109
|
+
attr_accessor :comment
|
110
|
+
|
111
|
+
# Close the archive. It is at this point that any changes made to the
|
112
|
+
# archive will be persisted to an output stream.
|
113
|
+
#
|
114
|
+
# Raises Archive::Zip::IOError if called more than once.
|
115
|
+
def close
|
116
|
+
raise IOError, 'closed archive' if closed?
|
117
|
+
|
118
|
+
if @dirty then
|
119
|
+
# There is something to write...
|
120
|
+
if @archive_out.nil? then
|
121
|
+
# Update the archive "in place".
|
122
|
+
tmp_archive_path = nil
|
123
|
+
Tempfile.open(*File.split(@archive_path).reverse) do |archive_out|
|
124
|
+
# Ensure the file is in binary mode for Windows.
|
125
|
+
archive_out.binmode
|
126
|
+
# Save off the path so that the temporary file can be renamed to the
|
127
|
+
# archive file later.
|
128
|
+
tmp_archive_path = archive_out.path
|
129
|
+
dump(archive_out)
|
130
|
+
end
|
131
|
+
File.chmod(0666 & ~File.umask, tmp_archive_path)
|
132
|
+
elsif @archive_out.kind_of?(String) then
|
133
|
+
# Open a new archive to receive the data.
|
134
|
+
File.open(@archive_out, 'wb') do |archive_out|
|
135
|
+
dump(archive_out)
|
136
|
+
end
|
137
|
+
else
|
138
|
+
# Assume the given object is an IO-like object and dump the archive
|
139
|
+
# contents to it.
|
140
|
+
dump(@archive_out)
|
141
|
+
end
|
142
|
+
@archive_in.close unless @archive_in.nil?
|
143
|
+
# The rename must happen after the original archive is closed when
|
144
|
+
# running on Windows since that platform does not allow a file which is
|
145
|
+
# in use to be replaced as is required when trying to update the archive
|
146
|
+
# "in place".
|
147
|
+
File.rename(tmp_archive_path, @archive_path) if @archive_out.nil?
|
148
|
+
end
|
149
|
+
|
150
|
+
closed = true
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns +true+ if the ZIP archive is closed, false otherwise.
|
155
|
+
def closed?
|
156
|
+
@closed
|
157
|
+
end
|
158
|
+
|
159
|
+
# When the ZIP archive is open, this method iterates through each entry in
|
160
|
+
# turn yielding each one to the given block. Since Zip includes Enumerable,
|
161
|
+
# Zip instances are enumerables of Entry instances.
|
162
|
+
#
|
163
|
+
# Raises Archive::Zip::IOError if called after #close.
|
164
|
+
def each(&b)
|
165
|
+
raise IOError, 'closed archive' if @closed
|
166
|
+
|
167
|
+
@entries.each_value(&b)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Add _entry_ into the ZIP archive replacing any existing entry with the
|
171
|
+
# same zip path.
|
172
|
+
#
|
173
|
+
# Raises Archive::Zip::IOError if called after #close.
|
174
|
+
def add_entry(entry)
|
175
|
+
raise IOError, 'closed archive' if @closed
|
176
|
+
unless entry.kind_of?(Entry) then
|
177
|
+
raise ArgumentError, 'Archive::Zip::Entry instance required'
|
178
|
+
end
|
179
|
+
|
180
|
+
@entries[entry.zip_path] = entry
|
181
|
+
@dirty = true
|
182
|
+
self
|
183
|
+
end
|
184
|
+
alias :<< :add_entry
|
185
|
+
|
186
|
+
# Look up an entry based on the zip path located in _zip_path_. Returns
|
187
|
+
# +nil+ if no entry is found.
|
188
|
+
def get_entry(zip_path)
|
189
|
+
@entries[zip_path]
|
190
|
+
end
|
191
|
+
alias :[] :get_entry
|
192
|
+
|
193
|
+
# Removes an entry from the ZIP file and returns the entry or +nil+ if no
|
194
|
+
# entry was found to remove. If _entry_ is an instance of
|
195
|
+
# Archive::Zip::Entry, the zip_path attribute is used to find the entry to
|
196
|
+
# remove; otherwise, _entry_ is assumed to be a zip path matching an entry
|
197
|
+
# in the ZIP archive.
|
198
|
+
#
|
199
|
+
# Raises Archive::Zip::IOError if called after #close.
|
200
|
+
def remove_entry(entry)
|
201
|
+
raise IOError, 'closed archive' if @closed
|
202
|
+
|
203
|
+
zip_path = entry
|
204
|
+
zip_path = entry.zip_path if entry.kind_of?(Entry)
|
205
|
+
entry = @entries.delete(zip_path)
|
206
|
+
entry = entry[1] unless entry.nil?
|
207
|
+
@dirty ||= ! entry.nil?
|
208
|
+
entry
|
209
|
+
end
|
210
|
+
|
211
|
+
# Adds _paths_ to the archive. _paths_ may be either a single path or an
|
212
|
+
# Array of paths. The files and directories referenced by _paths_ are added
|
213
|
+
# using their respective basenames as their zip paths. The exception to
|
214
|
+
# this is when the basename for a path is either <tt>"."</tt> or
|
215
|
+
# <tt>".."</tt>. In this case, the path is replaced with the paths to the
|
216
|
+
# contents of the directory it references.
|
217
|
+
#
|
218
|
+
# _options_ is a Hash optionally containing the following:
|
219
|
+
# <b>:path_prefix</b>::
|
220
|
+
# Specifies a prefix to be added to the zip_path attribute of each entry
|
221
|
+
# where `/' is the file separator character. This defaults to the empty
|
222
|
+
# string. All values are passed through Archive::Zip::Entry.expand_path
|
223
|
+
# before use.
|
224
|
+
# <b>:recursion</b>::
|
225
|
+
# When set to +true+ (the default), the contents of directories are
|
226
|
+
# recursively added to the archive.
|
227
|
+
# <b>:directories</b>::
|
228
|
+
# When set to +true+ (the default), entries are added to the archive for
|
229
|
+
# directories. Otherwise, the entries for directories will not be added;
|
230
|
+
# however, the contents of the directories will still be considered if the
|
231
|
+
# <b>:recursion</b> option is +true+.
|
232
|
+
# <b>:symlinks</b>::
|
233
|
+
# When set to +false+ (the default), entries for symlinks are excluded
|
234
|
+
# from the archive. Otherwise, they are included. <b>NOTE:</b> Unless
|
235
|
+
# <b>:follow_symlinks</b> is explicitly set, it will be set to the logical
|
236
|
+
# NOT of this option in calls to Archive::Zip::Entry.from_file. If
|
237
|
+
# symlinks should be completely ignored, set both this option and
|
238
|
+
# <b>:follow_symlinks</b> to +false+. See Archive::Zip::Entry.from_file
|
239
|
+
# for details regarding <b>:follow_symlinks</b>.
|
240
|
+
# <b>:flatten</b>::
|
241
|
+
# When set to +false+ (the default), the directory paths containing
|
242
|
+
# archived files will be included in the zip paths of entries representing
|
243
|
+
# the files. When set to +true+ files are archived without any containing
|
244
|
+
# directory structure in the zip paths. Setting to +true+ implies that
|
245
|
+
# <b>:directories</b> is +false+ and <b>:path_prefix</b> is empty.
|
246
|
+
# <b>:exclude</b>::
|
247
|
+
# Specifies a proc or lambda which takes a single argument containing a
|
248
|
+
# prospective zip entry and returns +true+ if the entry should be excluded
|
249
|
+
# from the archive and +false+ if it should be included. <b>NOTE:</b> If
|
250
|
+
# a directory is excluded in this way, the <b>:recursion</b> option has no
|
251
|
+
# effect for it.
|
252
|
+
# <b>:ignore_error</b>::
|
253
|
+
# When set to +false+ (the default), an error generated while creating an
|
254
|
+
# archive entry for a file will be raised. Otherwise, the bad file is
|
255
|
+
# skipped.
|
256
|
+
# Any other options which are supported by Archive::Zip::Entry.from_file are
|
257
|
+
# also supported.
|
258
|
+
#
|
259
|
+
# Raises Archive::Zip::IOError if called after #close. Raises
|
260
|
+
# Archive::Zip::EntryError if the <b>:ignore_error</b> option is +false+ and
|
261
|
+
# Archive::Zip::Entry.from_file raises an error.
|
262
|
+
#
|
263
|
+
# == Example
|
264
|
+
#
|
265
|
+
# A directory contains:
|
266
|
+
# zip-test
|
267
|
+
# +- dir1
|
268
|
+
# | +- file2.txt
|
269
|
+
# +- dir2
|
270
|
+
# +- file1.txt
|
271
|
+
#
|
272
|
+
# Create some archives:
|
273
|
+
# Archive::Zip.open('zip-test1.zip') do |z|
|
274
|
+
# z.archive('zip-test')
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# Archive::Zip.open('zip-test2.zip') do |z|
|
278
|
+
# z.archive('zip-test/.', :path_prefix => 'a/b/c/d')
|
279
|
+
# end
|
280
|
+
#
|
281
|
+
# Archive::Zip.open('zip-test3.zip') do |z|
|
282
|
+
# z.archive('zip-test', :directories => false)
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# Archive::Zip.open('zip-test4.zip') do |z|
|
286
|
+
# z.archive('zip-test', :exclude => lambda { |e| e.file? })
|
287
|
+
# end
|
288
|
+
#
|
289
|
+
# The archives contain:
|
290
|
+
# zip-test1.zip -> zip-test/
|
291
|
+
# zip-test/dir1/
|
292
|
+
# zip-test/dir1/file2.txt
|
293
|
+
# zip-test/dir2/
|
294
|
+
# zip-test/file1.txt
|
295
|
+
#
|
296
|
+
# zip-test2.zip -> a/b/c/d/dir1/
|
297
|
+
# a/b/c/d/dir1/file2.txt
|
298
|
+
# a/b/c/d/dir2/
|
299
|
+
# a/b/c/d/file1.txt
|
300
|
+
#
|
301
|
+
# zip-test3.zip -> zip-test/dir1/file2.txt
|
302
|
+
# zip-test/file1.txt
|
303
|
+
#
|
304
|
+
# zip-test4.zip -> zip-test/
|
305
|
+
# zip-test/dir1/
|
306
|
+
# zip-test/dir2/
|
307
|
+
def archive(paths, options = {})
|
308
|
+
raise IOError, 'closed archive' if @closed
|
309
|
+
|
310
|
+
# Ensure that paths is an enumerable.
|
311
|
+
paths = [paths] unless paths.kind_of?(Enumerable)
|
312
|
+
# If the basename of a path is '.' or '..', replace the path with the
|
313
|
+
# paths of all the entries contained within the directory referenced by
|
314
|
+
# the original path.
|
315
|
+
paths = paths.collect do |path|
|
316
|
+
basename = File.basename(path)
|
317
|
+
if basename == '.' || basename == '..' then
|
318
|
+
Dir.entries(path).reject do |e|
|
319
|
+
e == '.' || e == '..'
|
320
|
+
end.collect do |e|
|
321
|
+
File.join(path, e)
|
322
|
+
end
|
323
|
+
else
|
324
|
+
path
|
325
|
+
end
|
326
|
+
end.flatten.uniq
|
327
|
+
|
328
|
+
# Ensure that unspecified options have default values.
|
329
|
+
options[:path_prefix] = '' unless options.has_key?(:path_prefix)
|
330
|
+
options[:recursion] = true unless options.has_key?(:recursion)
|
331
|
+
options[:directories] = true unless options.has_key?(:directories)
|
332
|
+
options[:symlinks] = false unless options.has_key?(:symlinks)
|
333
|
+
options[:flatten] = false unless options.has_key?(:flatten)
|
334
|
+
options[:ignore_error] = false unless options.has_key?(:ignore_error)
|
335
|
+
|
336
|
+
# Flattening the directory structure implies that directories are skipped
|
337
|
+
# and that the path prefix should be ignored.
|
338
|
+
if options[:flatten] then
|
339
|
+
options[:path_prefix] = ''
|
340
|
+
options[:directories] = false
|
341
|
+
end
|
342
|
+
|
343
|
+
# Clean up the path prefix.
|
344
|
+
options[:path_prefix] = Entry.expand_path(options[:path_prefix].to_s)
|
345
|
+
|
346
|
+
paths.each do |path|
|
347
|
+
# Generate the zip path.
|
348
|
+
zip_entry_path = File.basename(path)
|
349
|
+
zip_entry_path += '/' if File.directory?(path)
|
350
|
+
unless options[:path_prefix].empty? then
|
351
|
+
zip_entry_path = "#{options[:path_prefix]}/#{zip_entry_path}"
|
352
|
+
end
|
353
|
+
|
354
|
+
begin
|
355
|
+
# Create the entry, but do not add it to the archive yet.
|
356
|
+
zip_entry = Zip::Entry.from_file(
|
357
|
+
path,
|
358
|
+
options.merge(
|
359
|
+
:zip_path => zip_entry_path,
|
360
|
+
:follow_symlinks => options.has_key?(:follow_symlinks) ?
|
361
|
+
options[:follow_symlinks] :
|
362
|
+
! options[:symlinks]
|
363
|
+
)
|
364
|
+
)
|
365
|
+
rescue Zip::EntryError
|
366
|
+
# Ignore the error if requested.
|
367
|
+
if options[:ignore_error] then
|
368
|
+
next
|
369
|
+
else
|
370
|
+
raise
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Skip this entry if so directed.
|
375
|
+
if (zip_entry.symlink? && ! options[:symlinks]) ||
|
376
|
+
(! options[:exclude].nil? && options[:exclude].call(zip_entry)) then
|
377
|
+
next
|
378
|
+
end
|
379
|
+
|
380
|
+
# Add entries for directories (if requested) and files/symlinks.
|
381
|
+
if (! zip_entry.directory? || options[:directories]) then
|
382
|
+
add_entry(zip_entry)
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
# Recurse into subdirectories (if requested).
|
387
|
+
if zip_entry.directory? && options[:recursion] then
|
388
|
+
archive(
|
389
|
+
Dir.entries(path).reject do |e|
|
390
|
+
e == '.' || e == '..'
|
391
|
+
end.collect do |e|
|
392
|
+
File.join(path, e)
|
393
|
+
end,
|
394
|
+
options.merge(:path_prefix => zip_entry_path)
|
395
|
+
)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
nil
|
400
|
+
end
|
401
|
+
|
402
|
+
# Extracts the contents of the archive to _destination_, where _destination_
|
403
|
+
# is a path to a directory which will contain the contents of the archive.
|
404
|
+
# The destination path will be created if it does not already exist.
|
405
|
+
#
|
406
|
+
# _options_ is a Hash optionally containing the following:
|
407
|
+
# <b>:directories</b>::
|
408
|
+
# When set to +true+ (the default), entries representing directories in
|
409
|
+
# the archive are extracted. This happens after all non-directory entries
|
410
|
+
# are extracted so that directory metadata can be properly updated.
|
411
|
+
# <b>:symlinks</b>::
|
412
|
+
# When set to +false+ (the default), entries representing symlinks in the
|
413
|
+
# archive are skipped. When set to +true+, such entries are extracted.
|
414
|
+
# Exceptions may be raised on plaforms/file systems which do not support
|
415
|
+
# symlinks.
|
416
|
+
# <b>:overwrite</b>::
|
417
|
+
# When set to <tt>:all</tt> (the default), files which already exist will
|
418
|
+
# be replaced. When set to <tt>:older</tt>, such files will only be
|
419
|
+
# replaced if they are older according to their last modified times than
|
420
|
+
# the zip entry which would replace them. When set to <tt>:none</tt>,
|
421
|
+
# such files will never be replaced. Any other value is the same as
|
422
|
+
# <tt>:all</tt>.
|
423
|
+
# <b>:create</b>::
|
424
|
+
# When set to +true+ (the default), files and directories which do not
|
425
|
+
# already exist will be extracted. When set to +false+ only files and
|
426
|
+
# directories which already exist will be extracted (depending on the
|
427
|
+
# setting of <b>:overwrite</b>).
|
428
|
+
# <b>:flatten</b>::
|
429
|
+
# When set to +false+ (the default), the directory paths containing
|
430
|
+
# extracted files will be created within +destination+ in order to contain
|
431
|
+
# the files. When set to +true+ files are extracted directly to
|
432
|
+
# +destination+ and directory entries are skipped.
|
433
|
+
# <b>:exclude</b>::
|
434
|
+
# Specifies a proc or lambda which takes a single argument containing a
|
435
|
+
# zip entry and returns +true+ if the entry should be skipped during
|
436
|
+
# extraction and +false+ if it should be extracted.
|
437
|
+
# Any other options which are supported by Archive::Zip::Entry#extract are
|
438
|
+
# also supported.
|
439
|
+
#
|
440
|
+
# Raises Archive::Zip::IOError if called after #close.
|
441
|
+
#
|
442
|
+
# == Example
|
443
|
+
#
|
444
|
+
# An archive, <tt>archive.zip</tt>, contains:
|
445
|
+
# zip-test/
|
446
|
+
# zip-test/dir1/
|
447
|
+
# zip-test/dir1/file2.txt
|
448
|
+
# zip-test/dir2/
|
449
|
+
# zip-test/file1.txt
|
450
|
+
#
|
451
|
+
# A directory, <tt>extract4</tt>, contains:
|
452
|
+
# zip-test
|
453
|
+
# +- dir1
|
454
|
+
# +- file1.txt
|
455
|
+
#
|
456
|
+
# Extract the archive:
|
457
|
+
# Archive::Zip.open('archive.zip') do |z|
|
458
|
+
# z.extract('extract1')
|
459
|
+
# end
|
460
|
+
#
|
461
|
+
# Archive::Zip.open('archive.zip') do |z|
|
462
|
+
# z.extract('extract2', :flatten => true)
|
463
|
+
# end
|
464
|
+
#
|
465
|
+
# Archive::Zip.open('archive.zip') do |z|
|
466
|
+
# z.extract('extract3', :create => false)
|
467
|
+
# end
|
468
|
+
#
|
469
|
+
# Archive::Zip.open('archive.zip') do |z|
|
470
|
+
# z.extract('extract3', :create => true)
|
471
|
+
# end
|
472
|
+
#
|
473
|
+
# Archive::Zip.open('archive.zip') do |z|
|
474
|
+
# z.extract( 'extract5', :exclude => lambda { |e| e.file? })
|
475
|
+
# end
|
476
|
+
#
|
477
|
+
# The directories contain:
|
478
|
+
# extract1 -> zip-test
|
479
|
+
# +- dir1
|
480
|
+
# | +- file2.txt
|
481
|
+
# +- dir2
|
482
|
+
# +- file1.txt
|
483
|
+
#
|
484
|
+
# extract2 -> file2.txt
|
485
|
+
# file1.txt
|
486
|
+
#
|
487
|
+
# extract3 -> <empty>
|
488
|
+
#
|
489
|
+
# extract4 -> zip-test
|
490
|
+
# +- dir2
|
491
|
+
# +- file1.txt <- from archive contents
|
492
|
+
#
|
493
|
+
# extract5 -> zip-test
|
494
|
+
# +- dir1
|
495
|
+
# +- dir2
|
496
|
+
def extract(destination, options = {})
|
497
|
+
raise IOError, 'closed archive' if @closed
|
498
|
+
|
499
|
+
# Ensure that unspecified options have default values.
|
500
|
+
options[:directories] = true unless options.has_key?(:directories)
|
501
|
+
options[:symlinks] = false unless options.has_key?(:symlinks)
|
502
|
+
options[:overwrite] = :all unless options[:overwrite] == :older ||
|
503
|
+
options[:overwrite] == :never
|
504
|
+
options[:create] = true unless options.has_key?(:create)
|
505
|
+
options[:flatten] = false unless options.has_key?(:flatten)
|
506
|
+
|
507
|
+
# Flattening the archive structure implies that directory entries are
|
508
|
+
# skipped.
|
509
|
+
options[:directories] = false if options[:flatten]
|
510
|
+
|
511
|
+
# First extract all non-directory entries.
|
512
|
+
directories = []
|
513
|
+
each do |entry|
|
514
|
+
# Compute the target file path.
|
515
|
+
file_path = entry.zip_path
|
516
|
+
file_path = File.basename(file_path) if options[:flatten]
|
517
|
+
file_path = File.join(destination, file_path)
|
518
|
+
|
519
|
+
# Cache some information about the file path.
|
520
|
+
file_exists = File.exist?(file_path)
|
521
|
+
file_mtime = File.mtime(file_path) if file_exists
|
522
|
+
|
523
|
+
# Skip this entry if so directed.
|
524
|
+
if (! file_exists && ! options[:create]) ||
|
525
|
+
(file_exists &&
|
526
|
+
(options[:overwrite] == :never ||
|
527
|
+
options[:overwrite] == :older && entry.mtime <= file_mtime)) ||
|
528
|
+
(! options[:exclude].nil? && options[:exclude].call(entry)) then
|
529
|
+
next
|
530
|
+
end
|
531
|
+
|
532
|
+
if entry.directory? then
|
533
|
+
# Record the directories as they are encountered.
|
534
|
+
directories << entry
|
535
|
+
elsif entry.file? || (entry.symlink? && options[:symlinks]) then
|
536
|
+
# Extract files and symlinks.
|
537
|
+
entry.extract(
|
538
|
+
options.merge(:file_path => file_path)
|
539
|
+
)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
if options[:directories] then
|
544
|
+
# Then extract the directory entries in depth first order so that time
|
545
|
+
# stamps, ownerships, and permissions can be properly restored.
|
546
|
+
directories.sort { |a, b| b.zip_path <=> a.zip_path }.each do |entry|
|
547
|
+
entry.extract(
|
548
|
+
options.merge(:file_path => File.join(destination, entry.zip_path))
|
549
|
+
)
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
|
556
|
+
private
|
557
|
+
|
558
|
+
# NOTE: For now _io_ MUST be seekable and report such by returning +true+
|
559
|
+
# from its seekable? method. See IO#seekable?.
|
560
|
+
#
|
561
|
+
# Raises Archive::Zip::IOError if _io_ is not seekable.
|
562
|
+
def parse(io)
|
563
|
+
# Error out if the IO object is not confirmed seekable.
|
564
|
+
raise Zip::IOError, 'non-seekable IO object given' unless io.respond_to?(:seekable?) and io.seekable?
|
565
|
+
|
566
|
+
socd_pos = find_central_directory(io)
|
567
|
+
io.seek(socd_pos)
|
568
|
+
# Parse each entry in the central directory.
|
569
|
+
loop do
|
570
|
+
signature = io.readbytes(4)
|
571
|
+
break unless signature == CFH_SIGNATURE
|
572
|
+
add_entry(Zip::Entry.parse(io))
|
573
|
+
end
|
574
|
+
@dirty = false
|
575
|
+
# Maybe add support for digital signatures and ZIP64 records... Later
|
576
|
+
|
577
|
+
nil
|
578
|
+
end
|
579
|
+
|
580
|
+
# Returns the file offset of the first record in the central directory.
|
581
|
+
# _io_ must be a seekable, readable, IO-like object.
|
582
|
+
#
|
583
|
+
# Raises Archive::Zip::UnzipError if the end of central directory signature
|
584
|
+
# is not found where expected or at all.
|
585
|
+
def find_central_directory(io)
|
586
|
+
# First find the offset to the end of central directory record.
|
587
|
+
# It is expected that the variable length comment field will usually be
|
588
|
+
# empty and as a result the initial value of eocd_offset is all that is
|
589
|
+
# necessary.
|
590
|
+
#
|
591
|
+
# NOTE: A cleverly crafted comment could throw this thing off if the
|
592
|
+
# comment itself looks like a valid end of central directory record.
|
593
|
+
eocd_offset = -22
|
594
|
+
loop do
|
595
|
+
io.seek(eocd_offset, IO::SEEK_END)
|
596
|
+
if io.readbytes(4) == EOCD_SIGNATURE then
|
597
|
+
io.seek(16, IO::SEEK_CUR)
|
598
|
+
break if io.readbytes(2).unpack('v')[0] == (eocd_offset + 22).abs
|
599
|
+
end
|
600
|
+
eocd_offset -= 1
|
601
|
+
end
|
602
|
+
# At this point, eocd_offset should point to the location of the end of
|
603
|
+
# central directory record relative to the end of the archive.
|
604
|
+
# Now, jump into the location in the record which contains a pointer to
|
605
|
+
# the start of the central directory record and return the value.
|
606
|
+
io.seek(eocd_offset + 16, IO::SEEK_END)
|
607
|
+
return io.readbytes(4).unpack('V')[0]
|
608
|
+
rescue Errno::EINVAL
|
609
|
+
raise Zip::UnzipError, 'unable to locate end-of-central-directory record'
|
610
|
+
end
|
611
|
+
|
612
|
+
# Writes all the entries of this archive to _io_. _io_ must be a writable,
|
613
|
+
# IO-like object providing a _write_ method. Returns the total number of
|
614
|
+
# bytes written.
|
615
|
+
def dump(io)
|
616
|
+
bytes_written = 0
|
617
|
+
entries = @entries.values
|
618
|
+
entries.each do |entry|
|
619
|
+
bytes_written += entry.dump_local_file_record(io, bytes_written)
|
620
|
+
end
|
621
|
+
central_directory_offset = bytes_written
|
622
|
+
entries.each do |entry|
|
623
|
+
bytes_written += entry.dump_central_file_record(io)
|
624
|
+
end
|
625
|
+
central_directory_length = bytes_written - central_directory_offset
|
626
|
+
bytes_written += io.write(EOCD_SIGNATURE)
|
627
|
+
bytes_written += io.write(
|
628
|
+
[
|
629
|
+
0,
|
630
|
+
0,
|
631
|
+
entries.length,
|
632
|
+
entries.length,
|
633
|
+
central_directory_length,
|
634
|
+
central_directory_offset,
|
635
|
+
comment.length
|
636
|
+
].pack('vvvvVVv')
|
637
|
+
)
|
638
|
+
bytes_written += io.write(comment)
|
639
|
+
|
640
|
+
bytes_written
|
641
|
+
end
|
642
|
+
end
|
643
|
+
end
|