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