rubyzip 2.3.1 → 3.0.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.
- checksums.yaml +4 -4
- data/Changelog.md +422 -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 +363 -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 +177 -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 +17 -23
- 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 -33
- data/TODO +0 -15
- data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb
CHANGED
@@ -1,124 +1,109 @@
|
|
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
|
-
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
|
59
54
|
|
55
|
+
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
|
56
|
+
|
57
|
+
# The name of this zip archive.
|
60
58
|
attr_reader :name
|
61
59
|
|
62
60
|
# default -> false.
|
63
61
|
attr_accessor :restore_ownership
|
64
62
|
|
65
|
-
# default ->
|
63
|
+
# default -> true.
|
66
64
|
attr_accessor :restore_permissions
|
67
65
|
|
68
|
-
# default ->
|
66
|
+
# default -> true.
|
69
67
|
attr_accessor :restore_times
|
70
68
|
|
71
|
-
|
72
|
-
attr_accessor :comment
|
69
|
+
def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
|
73
70
|
|
74
|
-
# Opens a zip archive. Pass true
|
71
|
+
# Opens a zip archive. Pass create: true to create
|
75
72
|
# a new archive if it doesn't exist already.
|
76
|
-
def initialize(path_or_io, create
|
73
|
+
def initialize(path_or_io, create: false, buffer: false,
|
74
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
75
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
76
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
77
|
+
compression_level: ::Zip.default_compression)
|
77
78
|
super()
|
78
|
-
|
79
|
+
|
79
80
|
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
|
80
|
-
@comment = ''
|
81
81
|
@create = create ? true : false # allow any truthy value to mean true
|
82
82
|
|
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
|
83
|
+
initialize_cdir(path_or_io, buffer: buffer)
|
108
84
|
|
109
|
-
@
|
110
|
-
@
|
111
|
-
@
|
112
|
-
@
|
113
|
-
@restore_times = options[:restore_times]
|
85
|
+
@restore_ownership = restore_ownership
|
86
|
+
@restore_permissions = restore_permissions
|
87
|
+
@restore_times = restore_times
|
88
|
+
@compression_level = compression_level
|
114
89
|
end
|
115
90
|
|
116
91
|
class << self
|
117
92
|
# Similar to ::new. If a block is passed the Zip::File object is passed
|
118
93
|
# to the block and is automatically closed afterwards, just as with
|
119
94
|
# ruby's builtin File::open method.
|
120
|
-
def open(file_name, create
|
121
|
-
|
95
|
+
def open(file_name, create: false,
|
96
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
97
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
98
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
99
|
+
compression_level: ::Zip.default_compression)
|
100
|
+
|
101
|
+
zf = ::Zip::File.new(file_name, create: create,
|
102
|
+
restore_ownership: restore_ownership,
|
103
|
+
restore_permissions: restore_permissions,
|
104
|
+
restore_times: restore_times,
|
105
|
+
compression_level: compression_level)
|
106
|
+
|
122
107
|
return zf unless block_given?
|
123
108
|
|
124
109
|
begin
|
@@ -128,29 +113,29 @@ module Zip
|
|
128
113
|
end
|
129
114
|
end
|
130
115
|
|
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
116
|
# Like #open, but reads zip archive contents from a String or open IO
|
140
117
|
# stream, and outputs data to a buffer.
|
141
118
|
# (This can be used to extract data from a
|
142
119
|
# downloaded zip archive without first saving it to disk.)
|
143
|
-
def open_buffer(io,
|
120
|
+
def open_buffer(io = ::StringIO.new, create: false,
|
121
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
122
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
123
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
124
|
+
compression_level: ::Zip.default_compression)
|
125
|
+
|
144
126
|
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
|
145
|
-
raise
|
127
|
+
raise 'Zip::File.open_buffer expects a String or IO-like argument' \
|
128
|
+
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
146
129
|
end
|
147
130
|
|
148
131
|
io = ::StringIO.new(io) if io.kind_of?(::String)
|
149
132
|
|
150
|
-
|
151
|
-
|
133
|
+
zf = ::Zip::File.new(io, create: create, buffer: true,
|
134
|
+
restore_ownership: restore_ownership,
|
135
|
+
restore_permissions: restore_permissions,
|
136
|
+
restore_times: restore_times,
|
137
|
+
compression_level: compression_level)
|
152
138
|
|
153
|
-
zf = ::Zip::File.new(io, true, true, options)
|
154
139
|
return zf unless block_given?
|
155
140
|
|
156
141
|
yield zf
|
@@ -174,81 +159,18 @@ module Zip
|
|
174
159
|
end
|
175
160
|
end
|
176
161
|
|
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
|
162
|
+
# Count the entries in a zip archive without reading the whole set of
|
163
|
+
# entry data into memory.
|
164
|
+
def count_entries(path_or_io)
|
165
|
+
cdir = ::Zip::CentralDirectory.new
|
205
166
|
|
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)
|
167
|
+
if path_or_io.kind_of?(String)
|
168
|
+
::File.open(path_or_io, 'rb') do |f|
|
169
|
+
cdir.count_entries(f)
|
248
170
|
end
|
171
|
+
else
|
172
|
+
cdir.count_entries(path_or_io)
|
249
173
|
end
|
250
|
-
::File.delete(zip_file_name) if delete_zip_file
|
251
|
-
szip_file_index
|
252
174
|
end
|
253
175
|
end
|
254
176
|
|
@@ -264,24 +186,29 @@ module Zip
|
|
264
186
|
# specified. If a block is passed the stream object is passed to the block and
|
265
187
|
# the stream is automatically closed afterwards just as with ruby's builtin
|
266
188
|
# File.open method.
|
267
|
-
def get_output_stream(entry,
|
268
|
-
extra
|
269
|
-
compression_method
|
270
|
-
&a_proc)
|
189
|
+
def get_output_stream(entry, permissions: nil, comment: nil,
|
190
|
+
extra: nil, compressed_size: nil, crc: nil,
|
191
|
+
compression_method: nil, compression_level: nil,
|
192
|
+
size: nil, time: nil, &a_proc)
|
271
193
|
|
272
194
|
new_entry =
|
273
195
|
if entry.kind_of?(Entry)
|
274
196
|
entry
|
275
197
|
else
|
276
|
-
Entry.new(
|
198
|
+
Entry.new(
|
199
|
+
@name, entry.to_s, comment: comment, extra: extra,
|
200
|
+
compressed_size: compressed_size, crc: crc, size: size,
|
201
|
+
compression_method: compression_method,
|
202
|
+
compression_level: compression_level, time: time
|
203
|
+
)
|
277
204
|
end
|
278
205
|
if new_entry.directory?
|
279
206
|
raise ArgumentError,
|
280
207
|
"cannot open stream to directory entry - '#{new_entry}'"
|
281
208
|
end
|
282
|
-
new_entry.unix_perms =
|
209
|
+
new_entry.unix_perms = permissions
|
283
210
|
zip_streamable_entry = StreamableStream.new(new_entry)
|
284
|
-
@
|
211
|
+
@cdir << zip_streamable_entry
|
285
212
|
zip_streamable_entry.get_output_stream(&a_proc)
|
286
213
|
end
|
287
214
|
|
@@ -299,31 +226,39 @@ module Zip
|
|
299
226
|
def add(entry, src_path, &continue_on_exists_proc)
|
300
227
|
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
|
301
228
|
check_entry_exists(entry, continue_on_exists_proc, 'add')
|
302
|
-
new_entry = entry.kind_of?(::Zip::Entry)
|
229
|
+
new_entry = if entry.kind_of?(::Zip::Entry)
|
230
|
+
entry
|
231
|
+
else
|
232
|
+
::Zip::Entry.new(
|
233
|
+
@name, entry.to_s,
|
234
|
+
compression_level: @compression_level
|
235
|
+
)
|
236
|
+
end
|
303
237
|
new_entry.gather_fileinfo_from_srcpath(src_path)
|
304
|
-
|
305
|
-
@entry_set << new_entry
|
238
|
+
@cdir << new_entry
|
306
239
|
end
|
307
240
|
|
308
241
|
# Convenience method for adding the contents of a file to the archive
|
309
242
|
# in Stored format (uncompressed)
|
310
243
|
def add_stored(entry, src_path, &continue_on_exists_proc)
|
311
|
-
entry = ::Zip::Entry.new(
|
244
|
+
entry = ::Zip::Entry.new(
|
245
|
+
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
|
246
|
+
)
|
312
247
|
add(entry, src_path, &continue_on_exists_proc)
|
313
248
|
end
|
314
249
|
|
315
250
|
# Removes the specified entry.
|
316
251
|
def remove(entry)
|
317
|
-
@
|
252
|
+
@cdir.delete(get_entry(entry))
|
318
253
|
end
|
319
254
|
|
320
255
|
# Renames the specified entry.
|
321
256
|
def rename(entry, new_name, &continue_on_exists_proc)
|
322
257
|
found_entry = get_entry(entry)
|
323
258
|
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
|
324
|
-
@
|
259
|
+
@cdir.delete(found_entry)
|
325
260
|
found_entry.name = new_name
|
326
|
-
@
|
261
|
+
@cdir << found_entry
|
327
262
|
end
|
328
263
|
|
329
264
|
# Replaces the specified entry with the contents of src_path (from
|
@@ -334,11 +269,16 @@ module Zip
|
|
334
269
|
add(entry, src_path)
|
335
270
|
end
|
336
271
|
|
337
|
-
# Extracts entry to file
|
338
|
-
|
272
|
+
# Extracts `entry` to a file at `entry_path`, with `destination_directory`
|
273
|
+
# as the base location in the filesystem.
|
274
|
+
#
|
275
|
+
# NB: The caller is responsible for making sure `destination_directory` is
|
276
|
+
# safe, if it is passed.
|
277
|
+
def extract(entry, entry_path = nil, destination_directory: '.', &block)
|
339
278
|
block ||= proc { ::Zip.on_exists_proc }
|
340
279
|
found_entry = get_entry(entry)
|
341
|
-
found_entry.
|
280
|
+
entry_path ||= found_entry.name
|
281
|
+
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
|
342
282
|
end
|
343
283
|
|
344
284
|
# Commits changes that has been made since the previous commit to
|
@@ -348,22 +288,23 @@ module Zip
|
|
348
288
|
|
349
289
|
on_success_replace do |tmp_file|
|
350
290
|
::Zip::OutputStream.open(tmp_file) do |zos|
|
351
|
-
@
|
291
|
+
@cdir.each do |e|
|
352
292
|
e.write_to_zip_output_stream(zos)
|
353
|
-
e.dirty = false
|
354
293
|
e.clean_up
|
355
294
|
end
|
356
295
|
zos.comment = comment
|
357
296
|
end
|
358
297
|
true
|
359
298
|
end
|
360
|
-
|
299
|
+
initialize_cdir(@name)
|
361
300
|
end
|
362
301
|
|
363
302
|
# Write buffer write changes to buffer and return
|
364
|
-
def write_buffer(io = ::StringIO.new
|
303
|
+
def write_buffer(io = ::StringIO.new)
|
304
|
+
return io unless commit_required?
|
305
|
+
|
365
306
|
::Zip::OutputStream.write_buffer(io) do |zos|
|
366
|
-
@
|
307
|
+
@cdir.each { |e| e.write_to_zip_output_stream(zos) }
|
367
308
|
zos.comment = comment
|
368
309
|
end
|
369
310
|
end
|
@@ -376,16 +317,19 @@ module Zip
|
|
376
317
|
# Returns true if any changes has been made to this archive since
|
377
318
|
# the previous commit
|
378
319
|
def commit_required?
|
379
|
-
@
|
380
|
-
|
320
|
+
return true if @create || @cdir.dirty?
|
321
|
+
|
322
|
+
@cdir.each do |e|
|
323
|
+
return true if e.dirty?
|
381
324
|
end
|
382
|
-
|
325
|
+
|
326
|
+
false
|
383
327
|
end
|
384
328
|
|
385
329
|
# Searches for entry with the specified name. Returns nil if
|
386
330
|
# no entry is found. See also get_entry
|
387
331
|
def find_entry(entry_name)
|
388
|
-
selected_entry = @
|
332
|
+
selected_entry = @cdir.find_entry(entry_name)
|
389
333
|
return if selected_entry.nil?
|
390
334
|
|
391
335
|
selected_entry.restore_ownership = @restore_ownership
|
@@ -394,11 +338,6 @@ module Zip
|
|
394
338
|
selected_entry
|
395
339
|
end
|
396
340
|
|
397
|
-
# Searches for entries given a glob
|
398
|
-
def glob(*args, &block)
|
399
|
-
@entry_set.glob(*args, &block)
|
400
|
-
end
|
401
|
-
|
402
341
|
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
403
342
|
# if no entry is found.
|
404
343
|
def get_entry(entry)
|
@@ -414,33 +353,50 @@ module Zip
|
|
414
353
|
|
415
354
|
entry_name = entry_name.dup.to_s
|
416
355
|
entry_name << '/' unless entry_name.end_with?('/')
|
417
|
-
@
|
356
|
+
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
|
418
357
|
end
|
419
358
|
|
420
359
|
private
|
421
360
|
|
422
|
-
def
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
361
|
+
def initialize_cdir(path_or_io, buffer: false)
|
362
|
+
@cdir = ::Zip::CentralDirectory.new
|
363
|
+
|
364
|
+
if ::File.size?(@name.to_s)
|
365
|
+
# There is a file, which exists, that is associated with this zip.
|
366
|
+
@create = false
|
367
|
+
@file_permissions = ::File.stat(@name).mode
|
368
|
+
|
369
|
+
if buffer
|
370
|
+
# https://github.com/rubyzip/rubyzip/issues/119
|
371
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
372
|
+
@cdir.read_from_stream(path_or_io)
|
373
|
+
else
|
374
|
+
::File.open(@name, 'rb') do |f|
|
375
|
+
@cdir.read_from_stream(f)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
elsif buffer && path_or_io.size > 0
|
379
|
+
# This zip is probably a non-empty StringIO.
|
380
|
+
@create = false
|
381
|
+
@cdir.read_from_stream(path_or_io)
|
382
|
+
elsif !@create && ::File.empty?(@name)
|
383
|
+
# A file exists, but it is empty, and we've said we're
|
384
|
+
# NOT creating a new zip.
|
385
|
+
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
386
|
+
elsif !@create
|
387
|
+
# If we get here, and we're not creating a new zip, then
|
388
|
+
# everything is wrong.
|
389
|
+
raise Error, "File #{@name} not found"
|
430
390
|
end
|
431
|
-
new_entry.directory? && path_is_directory
|
432
391
|
end
|
433
392
|
|
434
393
|
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
|
394
|
+
return unless @cdir.include?(entry_name)
|
395
|
+
|
435
396
|
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
|
436
|
-
|
397
|
+
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
|
437
398
|
|
438
|
-
|
439
|
-
remove get_entry(entry_name)
|
440
|
-
else
|
441
|
-
raise ::Zip::EntryExistsError,
|
442
|
-
proc_name + " failed. Entry #{entry_name} already exists"
|
443
|
-
end
|
399
|
+
remove get_entry(entry_name)
|
444
400
|
end
|
445
401
|
|
446
402
|
def check_file(path)
|
@@ -450,14 +406,12 @@ module Zip
|
|
450
406
|
def on_success_replace
|
451
407
|
dirname, basename = ::File.split(name)
|
452
408
|
::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)
|
409
|
+
if yield tmp_filename
|
410
|
+
::File.rename(tmp_filename, name)
|
411
|
+
::File.chmod(@file_permissions, name) unless @create
|
460
412
|
end
|
413
|
+
ensure
|
414
|
+
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
|
461
415
|
end
|
462
416
|
end
|
463
417
|
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
|