rubyzip 2.3.1 → 3.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +368 -0
  3. data/README.md +122 -39
  4. data/Rakefile +11 -7
  5. data/lib/zip/central_directory.rb +164 -118
  6. data/lib/zip/compressor.rb +3 -1
  7. data/lib/zip/constants.rb +25 -21
  8. data/lib/zip/crypto/decrypted_io.rb +3 -1
  9. data/lib/zip/crypto/encryption.rb +4 -2
  10. data/lib/zip/crypto/null_encryption.rb +5 -3
  11. data/lib/zip/crypto/traditional_encryption.rb +5 -3
  12. data/lib/zip/decompressor.rb +4 -3
  13. data/lib/zip/deflater.rb +10 -8
  14. data/lib/zip/dirtyable.rb +32 -0
  15. data/lib/zip/dos_time.rb +41 -5
  16. data/lib/zip/entry.rb +273 -170
  17. data/lib/zip/entry_set.rb +9 -7
  18. data/lib/zip/errors.rb +115 -16
  19. data/lib/zip/extra_field/generic.rb +3 -10
  20. data/lib/zip/extra_field/ntfs.rb +4 -2
  21. data/lib/zip/extra_field/old_unix.rb +3 -1
  22. data/lib/zip/extra_field/universal_time.rb +3 -1
  23. data/lib/zip/extra_field/unix.rb +5 -3
  24. data/lib/zip/extra_field/unknown.rb +33 -0
  25. data/lib/zip/extra_field/zip64.rb +12 -5
  26. data/lib/zip/extra_field.rb +15 -21
  27. data/lib/zip/file.rb +152 -221
  28. data/lib/zip/file_split.rb +97 -0
  29. data/lib/zip/filesystem/dir.rb +86 -0
  30. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  31. data/lib/zip/filesystem/file.rb +262 -0
  32. data/lib/zip/filesystem/file_stat.rb +110 -0
  33. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  34. data/lib/zip/filesystem.rb +26 -595
  35. data/lib/zip/inflater.rb +6 -4
  36. data/lib/zip/input_stream.rb +43 -25
  37. data/lib/zip/ioextras/abstract_input_stream.rb +13 -8
  38. data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
  39. data/lib/zip/ioextras.rb +6 -6
  40. data/lib/zip/null_compressor.rb +3 -1
  41. data/lib/zip/null_decompressor.rb +3 -1
  42. data/lib/zip/null_input_stream.rb +3 -1
  43. data/lib/zip/output_stream.rb +45 -39
  44. data/lib/zip/pass_thru_compressor.rb +3 -1
  45. data/lib/zip/pass_thru_decompressor.rb +4 -2
  46. data/lib/zip/streamable_directory.rb +3 -1
  47. data/lib/zip/streamable_stream.rb +3 -0
  48. data/lib/zip/version.rb +3 -1
  49. data/lib/zip.rb +15 -22
  50. data/rubyzip.gemspec +38 -0
  51. data/samples/example.rb +8 -3
  52. data/samples/example_filesystem.rb +2 -1
  53. data/samples/example_recursive.rb +3 -1
  54. data/samples/gtk_ruby_zip.rb +4 -2
  55. data/samples/qtzip.rb +6 -5
  56. data/samples/write_simple.rb +1 -0
  57. data/samples/zipfind.rb +1 -0
  58. metadata +83 -35
  59. data/TODO +0 -15
  60. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb CHANGED
@@ -1,124 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative 'file_split'
6
+
1
7
  module Zip
2
- # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
3
- # The most important methods are those inherited from
4
- # ZipCentralDirectory for accessing information about the entries in
5
- # the archive and methods such as get_input_stream and
6
- # get_output_stream for reading from and writing entries to the
8
+ # Zip::File is modeled after java.util.zip.ZipFile from the Java SDK.
9
+ # The most important methods are those for accessing information about
10
+ # the entries in
11
+ # the archive and methods such as `get_input_stream` and
12
+ # `get_output_stream` for reading from and writing entries to the
7
13
  # archive. The class includes a few convenience methods such as
8
- # #extract for extracting entries to the filesystem, and #remove,
9
- # #replace, #rename and #mkdir for making simple modifications to
14
+ # `extract` for extracting entries to the filesystem, and `remove`,
15
+ # `replace`, `rename` and `mkdir` for making simple modifications to
10
16
  # the archive.
11
17
  #
12
- # Modifications to a zip archive are not committed until #commit or
13
- # #close is called. The method #open accepts a block following
14
- # the pattern from File.open offering a simple way to
18
+ # Modifications to a zip archive are not committed until `commit` or
19
+ # `close` is called. The method `open` accepts a block following
20
+ # the pattern from ::File.open offering a simple way to
15
21
  # automatically close the archive when the block returns.
16
22
  #
17
- # The following example opens zip archive <code>my.zip</code>
23
+ # The following example opens zip archive `my.zip`
18
24
  # (creating it if it doesn't exist) and adds an entry
19
- # <code>first.txt</code> and a directory entry <code>a_dir</code>
25
+ # `first.txt` and a directory entry `a_dir`
20
26
  # to it.
21
27
  #
22
- # require 'zip'
28
+ # ```
29
+ # require 'zip'
23
30
  #
24
- # Zip::File.open("my.zip", Zip::File::CREATE) {
25
- # |zipfile|
26
- # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
27
- # zipfile.mkdir("a_dir")
28
- # }
31
+ # Zip::File.open('my.zip', create: true) do |zipfile|
32
+ # zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' }
33
+ # zipfile.mkdir('a_dir')
34
+ # end
35
+ # ```
29
36
  #
30
- # The next example reopens <code>my.zip</code> writes the contents of
31
- # <code>first.txt</code> to standard out and deletes the entry from
37
+ # The next example reopens `my.zip`, writes the contents of
38
+ # `first.txt` to standard out and deletes the entry from
32
39
  # the archive.
33
40
  #
34
- # require 'zip'
41
+ # ```
42
+ # require 'zip'
35
43
  #
36
- # Zip::File.open("my.zip", Zip::File::CREATE) {
37
- # |zipfile|
38
- # puts zipfile.read("first.txt")
39
- # zipfile.remove("first.txt")
40
- # }
44
+ # Zip::File.open('my.zip', create: true) do |zipfile|
45
+ # puts zipfile.read('first.txt')
46
+ # zipfile.remove('first.txt')
47
+ # end
41
48
  #
42
- # ZipFileSystem offers an alternative API that emulates ruby's
43
- # interface for accessing the filesystem, ie. the File and Dir classes.
44
-
45
- class File < CentralDirectory
46
- CREATE = true
47
- SPLIT_SIGNATURE = 0x08074b50
48
- ZIP64_EOCD_SIGNATURE = 0x06064b50
49
- MAX_SEGMENT_SIZE = 3_221_225_472
50
- MIN_SEGMENT_SIZE = 65_536
51
- DATA_BUFFER_SIZE = 8192
52
- IO_METHODS = [:tell, :seek, :read, :eof, :close]
53
-
54
- DEFAULT_OPTIONS = {
55
- restore_ownership: false,
56
- restore_permissions: false,
57
- restore_times: false
58
- }.freeze
49
+ # Zip::FileSystem offers an alternative API that emulates ruby's
50
+ # interface for accessing the filesystem, ie. the ::File and ::Dir classes.
51
+ class File
52
+ extend Forwardable
53
+ extend FileSplit
54
+
55
+ IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze
59
56
 
60
57
  attr_reader :name
61
58
 
62
59
  # default -> false.
63
60
  attr_accessor :restore_ownership
64
61
 
65
- # default -> false, but will be set to true in a future version.
62
+ # default -> true.
66
63
  attr_accessor :restore_permissions
67
64
 
68
- # default -> false, but will be set to true in a future version.
65
+ # default -> true.
69
66
  attr_accessor :restore_times
70
67
 
71
- # Returns the zip files comment, if it has one
72
- attr_accessor :comment
68
+ def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
73
69
 
74
- # Opens a zip archive. Pass true as the second parameter to create
70
+ # Opens a zip archive. Pass create: true to create
75
71
  # a new archive if it doesn't exist already.
76
- def initialize(path_or_io, create = false, buffer = false, options = {})
72
+ def initialize(path_or_io, create: false, buffer: false, **options)
77
73
  super()
78
- options = DEFAULT_OPTIONS.merge(options)
74
+ options = DEFAULT_RESTORE_OPTIONS
75
+ .merge(compression_level: ::Zip.default_compression)
76
+ .merge(options)
79
77
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
80
- @comment = ''
81
78
  @create = create ? true : false # allow any truthy value to mean true
82
79
 
83
- if ::File.size?(@name.to_s)
84
- # There is a file, which exists, that is associated with this zip.
85
- @create = false
86
- @file_permissions = ::File.stat(@name).mode
80
+ initialize_cdir(path_or_io, buffer: buffer)
87
81
 
88
- if buffer
89
- read_from_stream(path_or_io)
90
- else
91
- ::File.open(@name, 'rb') do |f|
92
- read_from_stream(f)
93
- end
94
- end
95
- elsif buffer && path_or_io.size > 0
96
- # This zip is probably a non-empty StringIO.
97
- read_from_stream(path_or_io)
98
- elsif @create
99
- # This zip is completely new/empty and is to be created.
100
- @entry_set = EntrySet.new
101
- elsif ::File.zero?(@name)
102
- # A file exists, but it is empty.
103
- raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
104
- else
105
- # Everything is wrong.
106
- raise Error, "File #{@name} not found"
107
- end
108
-
109
- @stored_entries = @entry_set.dup
110
- @stored_comment = @comment
111
82
  @restore_ownership = options[:restore_ownership]
112
83
  @restore_permissions = options[:restore_permissions]
113
84
  @restore_times = options[:restore_times]
85
+ @compression_level = options[:compression_level]
114
86
  end
115
87
 
116
88
  class << self
117
89
  # Similar to ::new. If a block is passed the Zip::File object is passed
118
90
  # to the block and is automatically closed afterwards, just as with
119
91
  # ruby's builtin File::open method.
120
- def open(file_name, create = false, options = {})
121
- zf = ::Zip::File.new(file_name, create, false, options)
92
+ def open(file_name, create: false, **options)
93
+ zf = ::Zip::File.new(file_name, create: create, **options)
122
94
  return zf unless block_given?
123
95
 
124
96
  begin
@@ -128,29 +100,19 @@ module Zip
128
100
  end
129
101
  end
130
102
 
131
- # Same as #open. But outputs data to a buffer instead of a file
132
- def add_buffer
133
- io = ::StringIO.new('')
134
- zf = ::Zip::File.new(io, true, true)
135
- yield zf
136
- zf.write_buffer(io)
137
- end
138
-
139
103
  # Like #open, but reads zip archive contents from a String or open IO
140
104
  # stream, and outputs data to a buffer.
141
105
  # (This can be used to extract data from a
142
106
  # downloaded zip archive without first saving it to disk.)
143
- def open_buffer(io, options = {})
107
+ def open_buffer(io = ::StringIO.new, create: false, **options)
144
108
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
145
- raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
109
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
110
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
146
111
  end
147
112
 
148
113
  io = ::StringIO.new(io) if io.kind_of?(::String)
149
114
 
150
- # https://github.com/rubyzip/rubyzip/issues/119
151
- io.binmode if io.respond_to?(:binmode)
152
-
153
- zf = ::Zip::File.new(io, true, true, options)
115
+ zf = ::Zip::File.new(io, create: create, buffer: true, **options)
154
116
  return zf unless block_given?
155
117
 
156
118
  yield zf
@@ -174,81 +136,18 @@ module Zip
174
136
  end
175
137
  end
176
138
 
177
- def get_segment_size_for_split(segment_size)
178
- if MIN_SEGMENT_SIZE > segment_size
179
- MIN_SEGMENT_SIZE
180
- elsif MAX_SEGMENT_SIZE < segment_size
181
- MAX_SEGMENT_SIZE
182
- else
183
- segment_size
184
- end
185
- end
186
-
187
- def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
188
- unless partial_zip_file_name.nil?
189
- partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
190
- partial_zip_file_name + ::File.extname(zip_file_name))
191
- end
192
- partial_zip_file_name ||= zip_file_name
193
- partial_zip_file_name
194
- end
195
-
196
- def get_segment_count_for_split(zip_file_size, segment_size)
197
- (zip_file_size / segment_size).to_i + (zip_file_size % segment_size == 0 ? 0 : 1)
198
- end
199
-
200
- def put_split_signature(szip_file, segment_size)
201
- signature_packed = [SPLIT_SIGNATURE].pack('V')
202
- szip_file << signature_packed
203
- segment_size - signature_packed.size
204
- end
205
-
206
- #
207
- # TODO: Make the code more understandable
208
- #
209
- def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
210
- ssegment_size = zip_file_size - zip_file.pos
211
- ssegment_size = segment_size if ssegment_size > segment_size
212
- szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
213
- ::File.open(szip_file_name, 'wb') do |szip_file|
214
- if szip_file_index == 1
215
- ssegment_size = put_split_signature(szip_file, segment_size)
216
- end
217
- chunk_bytes = 0
218
- until ssegment_size == chunk_bytes || zip_file.eof?
219
- segment_bytes_left = ssegment_size - chunk_bytes
220
- buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
221
- chunk = zip_file.read(buffer_size)
222
- chunk_bytes += buffer_size
223
- szip_file << chunk
224
- # Info for track splitting
225
- yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
226
- end
227
- end
228
- end
139
+ # Count the entries in a zip archive without reading the whole set of
140
+ # entry data into memory.
141
+ def count_entries(path_or_io)
142
+ cdir = ::Zip::CentralDirectory.new
229
143
 
230
- # Splits an archive into parts with segment size
231
- def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true, partial_zip_file_name = nil)
232
- raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
233
- raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
234
-
235
- zip_file_size = ::File.size(zip_file_name)
236
- segment_size = get_segment_size_for_split(segment_size)
237
- return if zip_file_size <= segment_size
238
-
239
- segment_count = get_segment_count_for_split(zip_file_size, segment_size)
240
- # Checking for correct zip structure
241
- ::Zip::File.open(zip_file_name) {}
242
- partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
243
- szip_file_index = 0
244
- ::File.open(zip_file_name, 'rb') do |zip_file|
245
- until zip_file.eof?
246
- szip_file_index += 1
247
- save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
144
+ if path_or_io.kind_of?(String)
145
+ ::File.open(path_or_io, 'rb') do |f|
146
+ cdir.count_entries(f)
248
147
  end
148
+ else
149
+ cdir.count_entries(path_or_io)
249
150
  end
250
- ::File.delete(zip_file_name) if delete_zip_file
251
- szip_file_index
252
151
  end
253
152
  end
254
153
 
@@ -264,24 +163,29 @@ module Zip
264
163
  # specified. If a block is passed the stream object is passed to the block and
265
164
  # the stream is automatically closed afterwards just as with ruby's builtin
266
165
  # File.open method.
267
- def get_output_stream(entry, permission_int = nil, comment = nil,
268
- extra = nil, compressed_size = nil, crc = nil,
269
- compression_method = nil, size = nil, time = nil,
270
- &a_proc)
166
+ def get_output_stream(entry, permissions: nil, comment: nil,
167
+ extra: nil, compressed_size: nil, crc: nil,
168
+ compression_method: nil, compression_level: nil,
169
+ size: nil, time: nil, &a_proc)
271
170
 
272
171
  new_entry =
273
172
  if entry.kind_of?(Entry)
274
173
  entry
275
174
  else
276
- Entry.new(@name, entry.to_s, comment, extra, compressed_size, crc, compression_method, size, time)
175
+ Entry.new(
176
+ @name, entry.to_s, comment: comment, extra: extra,
177
+ compressed_size: compressed_size, crc: crc, size: size,
178
+ compression_method: compression_method,
179
+ compression_level: compression_level, time: time
180
+ )
277
181
  end
278
182
  if new_entry.directory?
279
183
  raise ArgumentError,
280
184
  "cannot open stream to directory entry - '#{new_entry}'"
281
185
  end
282
- new_entry.unix_perms = permission_int
186
+ new_entry.unix_perms = permissions
283
187
  zip_streamable_entry = StreamableStream.new(new_entry)
284
- @entry_set << zip_streamable_entry
188
+ @cdir << zip_streamable_entry
285
189
  zip_streamable_entry.get_output_stream(&a_proc)
286
190
  end
287
191
 
@@ -299,31 +203,39 @@ module Zip
299
203
  def add(entry, src_path, &continue_on_exists_proc)
300
204
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
301
205
  check_entry_exists(entry, continue_on_exists_proc, 'add')
302
- new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
206
+ new_entry = if entry.kind_of?(::Zip::Entry)
207
+ entry
208
+ else
209
+ ::Zip::Entry.new(
210
+ @name, entry.to_s,
211
+ compression_level: @compression_level
212
+ )
213
+ end
303
214
  new_entry.gather_fileinfo_from_srcpath(src_path)
304
- new_entry.dirty = true
305
- @entry_set << new_entry
215
+ @cdir << new_entry
306
216
  end
307
217
 
308
218
  # Convenience method for adding the contents of a file to the archive
309
219
  # in Stored format (uncompressed)
310
220
  def add_stored(entry, src_path, &continue_on_exists_proc)
311
- entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
221
+ entry = ::Zip::Entry.new(
222
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
223
+ )
312
224
  add(entry, src_path, &continue_on_exists_proc)
313
225
  end
314
226
 
315
227
  # Removes the specified entry.
316
228
  def remove(entry)
317
- @entry_set.delete(get_entry(entry))
229
+ @cdir.delete(get_entry(entry))
318
230
  end
319
231
 
320
232
  # Renames the specified entry.
321
233
  def rename(entry, new_name, &continue_on_exists_proc)
322
234
  found_entry = get_entry(entry)
323
235
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
324
- @entry_set.delete(found_entry)
236
+ @cdir.delete(found_entry)
325
237
  found_entry.name = new_name
326
- @entry_set << found_entry
238
+ @cdir << found_entry
327
239
  end
328
240
 
329
241
  # Replaces the specified entry with the contents of src_path (from
@@ -334,11 +246,16 @@ module Zip
334
246
  add(entry, src_path)
335
247
  end
336
248
 
337
- # Extracts entry to file dest_path.
338
- def extract(entry, dest_path, &block)
249
+ # Extracts `entry` to a file at `entry_path`, with `destination_directory`
250
+ # as the base location in the filesystem.
251
+ #
252
+ # NB: The caller is responsible for making sure `destination_directory` is
253
+ # safe, if it is passed.
254
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
339
255
  block ||= proc { ::Zip.on_exists_proc }
340
256
  found_entry = get_entry(entry)
341
- found_entry.extract(dest_path, &block)
257
+ entry_path ||= found_entry.name
258
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
342
259
  end
343
260
 
344
261
  # Commits changes that has been made since the previous commit to
@@ -348,22 +265,23 @@ module Zip
348
265
 
349
266
  on_success_replace do |tmp_file|
350
267
  ::Zip::OutputStream.open(tmp_file) do |zos|
351
- @entry_set.each do |e|
268
+ @cdir.each do |e|
352
269
  e.write_to_zip_output_stream(zos)
353
- e.dirty = false
354
270
  e.clean_up
355
271
  end
356
272
  zos.comment = comment
357
273
  end
358
274
  true
359
275
  end
360
- initialize(name)
276
+ initialize_cdir(@name)
361
277
  end
362
278
 
363
279
  # Write buffer write changes to buffer and return
364
- def write_buffer(io = ::StringIO.new(''))
280
+ def write_buffer(io = ::StringIO.new)
281
+ return unless commit_required?
282
+
365
283
  ::Zip::OutputStream.write_buffer(io) do |zos|
366
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
284
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
367
285
  zos.comment = comment
368
286
  end
369
287
  end
@@ -376,16 +294,19 @@ module Zip
376
294
  # Returns true if any changes has been made to this archive since
377
295
  # the previous commit
378
296
  def commit_required?
379
- @entry_set.each do |e|
380
- return true if e.dirty
297
+ return true if @create || @cdir.dirty?
298
+
299
+ @cdir.each do |e|
300
+ return true if e.dirty?
381
301
  end
382
- @comment != @stored_comment || @entry_set != @stored_entries || @create
302
+
303
+ false
383
304
  end
384
305
 
385
306
  # Searches for entry with the specified name. Returns nil if
386
307
  # no entry is found. See also get_entry
387
308
  def find_entry(entry_name)
388
- selected_entry = @entry_set.find_entry(entry_name)
309
+ selected_entry = @cdir.find_entry(entry_name)
389
310
  return if selected_entry.nil?
390
311
 
391
312
  selected_entry.restore_ownership = @restore_ownership
@@ -394,11 +315,6 @@ module Zip
394
315
  selected_entry
395
316
  end
396
317
 
397
- # Searches for entries given a glob
398
- def glob(*args, &block)
399
- @entry_set.glob(*args, &block)
400
- end
401
-
402
318
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
403
319
  # if no entry is found.
404
320
  def get_entry(entry)
@@ -414,33 +330,50 @@ module Zip
414
330
 
415
331
  entry_name = entry_name.dup.to_s
416
332
  entry_name << '/' unless entry_name.end_with?('/')
417
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
333
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
418
334
  end
419
335
 
420
336
  private
421
337
 
422
- def directory?(new_entry, src_path)
423
- path_is_directory = ::File.directory?(src_path)
424
- if new_entry.directory? && !path_is_directory
425
- raise ArgumentError,
426
- "entry name '#{new_entry}' indicates directory entry, but " \
427
- "'#{src_path}' is not a directory"
428
- elsif !new_entry.directory? && path_is_directory
429
- new_entry.name += '/'
338
+ def initialize_cdir(path_or_io, buffer: false)
339
+ @cdir = ::Zip::CentralDirectory.new
340
+
341
+ if ::File.size?(@name.to_s)
342
+ # There is a file, which exists, that is associated with this zip.
343
+ @create = false
344
+ @file_permissions = ::File.stat(@name).mode
345
+
346
+ if buffer
347
+ # https://github.com/rubyzip/rubyzip/issues/119
348
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
349
+ @cdir.read_from_stream(path_or_io)
350
+ else
351
+ ::File.open(@name, 'rb') do |f|
352
+ @cdir.read_from_stream(f)
353
+ end
354
+ end
355
+ elsif buffer && path_or_io.size > 0
356
+ # This zip is probably a non-empty StringIO.
357
+ @create = false
358
+ @cdir.read_from_stream(path_or_io)
359
+ elsif !@create && ::File.zero?(@name)
360
+ # A file exists, but it is empty, and we've said we're
361
+ # NOT creating a new zip.
362
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
363
+ elsif !@create
364
+ # If we get here, and we're not creating a new zip, then
365
+ # everything is wrong.
366
+ raise Error, "File #{@name} not found"
430
367
  end
431
- new_entry.directory? && path_is_directory
432
368
  end
433
369
 
434
370
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
371
+ return unless @cdir.include?(entry_name)
372
+
435
373
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
436
- return unless @entry_set.include?(entry_name)
374
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
437
375
 
438
- if continue_on_exists_proc.call
439
- remove get_entry(entry_name)
440
- else
441
- raise ::Zip::EntryExistsError,
442
- proc_name + " failed. Entry #{entry_name} already exists"
443
- end
376
+ remove get_entry(entry_name)
444
377
  end
445
378
 
446
379
  def check_file(path)
@@ -450,14 +383,12 @@ module Zip
450
383
  def on_success_replace
451
384
  dirname, basename = ::File.split(name)
452
385
  ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
453
- begin
454
- if yield tmp_filename
455
- ::File.rename(tmp_filename, name)
456
- ::File.chmod(@file_permissions, name) unless @create
457
- end
458
- ensure
459
- ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
386
+ if yield tmp_filename
387
+ ::File.rename(tmp_filename, name)
388
+ ::File.chmod(@file_permissions, name) unless @create
460
389
  end
390
+ ensure
391
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
461
392
  end
462
393
  end
463
394
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zip
4
+ module FileSplit # :nodoc:
5
+ MAX_SEGMENT_SIZE = 3_221_225_472
6
+ MIN_SEGMENT_SIZE = 65_536
7
+ DATA_BUFFER_SIZE = 8192
8
+
9
+ def get_segment_size_for_split(segment_size)
10
+ if MIN_SEGMENT_SIZE > segment_size
11
+ MIN_SEGMENT_SIZE
12
+ elsif MAX_SEGMENT_SIZE < segment_size
13
+ MAX_SEGMENT_SIZE
14
+ else
15
+ segment_size
16
+ end
17
+ end
18
+
19
+ def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
20
+ unless partial_zip_file_name.nil?
21
+ partial_zip_file_name = zip_file_name.sub(
22
+ /#{::File.basename(zip_file_name)}\z/,
23
+ partial_zip_file_name + ::File.extname(zip_file_name)
24
+ )
25
+ end
26
+ partial_zip_file_name ||= zip_file_name
27
+ partial_zip_file_name
28
+ end
29
+
30
+ def get_segment_count_for_split(zip_file_size, segment_size)
31
+ (zip_file_size / segment_size).to_i +
32
+ ((zip_file_size % segment_size).zero? ? 0 : 1)
33
+ end
34
+
35
+ def put_split_signature(szip_file, segment_size)
36
+ signature_packed = [SPLIT_FILE_SIGNATURE].pack('V')
37
+ szip_file << signature_packed
38
+ segment_size - signature_packed.size
39
+ end
40
+
41
+ #
42
+ # TODO: Make the code more understandable
43
+ #
44
+ def save_splited_part(
45
+ zip_file, partial_zip_file_name, zip_file_size,
46
+ szip_file_index, segment_size, segment_count
47
+ )
48
+ ssegment_size = zip_file_size - zip_file.pos
49
+ ssegment_size = segment_size if ssegment_size > segment_size
50
+ szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
51
+ ::File.open(szip_file_name, 'wb') do |szip_file|
52
+ if szip_file_index == 1
53
+ ssegment_size = put_split_signature(szip_file, segment_size)
54
+ end
55
+ chunk_bytes = 0
56
+ until ssegment_size == chunk_bytes || zip_file.eof?
57
+ segment_bytes_left = ssegment_size - chunk_bytes
58
+ buffer_size = [segment_bytes_left, DATA_BUFFER_SIZE].min
59
+ chunk = zip_file.read(buffer_size)
60
+ chunk_bytes += buffer_size
61
+ szip_file << chunk
62
+ # Info for track splitting
63
+ yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
64
+ end
65
+ end
66
+ end
67
+
68
+ # Splits an archive into parts with segment size
69
+ def split(
70
+ zip_file_name, segment_size: MAX_SEGMENT_SIZE,
71
+ delete_original: true, partial_zip_file_name: nil
72
+ )
73
+ raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
74
+ raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
75
+
76
+ zip_file_size = ::File.size(zip_file_name)
77
+ segment_size = get_segment_size_for_split(segment_size)
78
+ return if zip_file_size <= segment_size
79
+
80
+ segment_count = get_segment_count_for_split(zip_file_size, segment_size)
81
+ ::Zip::File.open(zip_file_name) {} # Check for correct zip structure.
82
+ partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
83
+ szip_file_index = 0
84
+ ::File.open(zip_file_name, 'rb') do |zip_file|
85
+ until zip_file.eof?
86
+ szip_file_index += 1
87
+ save_splited_part(
88
+ zip_file, partial_zip_file_name, zip_file_size,
89
+ szip_file_index, segment_size, segment_count
90
+ )
91
+ end
92
+ end
93
+ ::File.delete(zip_file_name) if delete_original
94
+ szip_file_index
95
+ end
96
+ end
97
+ end