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