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.
@@ -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
@@ -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