rubyzip 2.3.1 → 3.0.0.alpha

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