rubyzip 2.1.0 → 3.6.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 +538 -0
- data/LICENSE.md +24 -0
- data/README.md +235 -48
- data/Rakefile +17 -12
- data/lib/rubyzip.rb +3 -0
- data/lib/zip/central_directory.rb +196 -123
- data/lib/zip/compressor.rb +3 -1
- data/lib/zip/constants.rb +81 -21
- data/lib/zip/crypto/aes_encryption.rb +123 -0
- data/lib/zip/crypto/decrypted_io.rb +52 -0
- data/lib/zip/crypto/encryption.rb +4 -2
- data/lib/zip/crypto/null_encryption.rb +5 -13
- data/lib/zip/crypto/traditional_encryption.rb +22 -16
- data/lib/zip/decompressor.rb +21 -2
- data/lib/zip/deflater.rb +12 -8
- data/lib/zip/dirtyable.rb +32 -0
- data/lib/zip/dos_time.rb +79 -14
- data/lib/zip/entry.rb +430 -249
- data/lib/zip/entry_set.rb +13 -9
- data/lib/zip/errors.rb +152 -15
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +17 -17
- data/lib/zip/extra_field/ntfs.rb +10 -4
- data/lib/zip/extra_field/old_unix.rb +8 -4
- data/lib/zip/extra_field/universal_time.rb +6 -1
- data/lib/zip/extra_field/unix.rb +9 -5
- data/lib/zip/extra_field/unknown.rb +35 -0
- data/lib/zip/extra_field/zip64.rb +23 -7
- data/lib/zip/extra_field.rb +31 -27
- data/lib/zip/file.rb +211 -241
- 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 +32 -585
- data/lib/zip/inflater.rb +28 -37
- data/lib/zip/input_stream.rb +113 -54
- data/lib/zip/ioextras/abstract_input_stream.rb +154 -55
- data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
- data/lib/zip/ioextras.rb +12 -9
- data/lib/zip/null_compressor.rb +3 -1
- data/lib/zip/null_decompressor.rb +7 -12
- data/lib/zip/null_input_stream.rb +3 -1
- data/lib/zip/output_stream.rb +76 -51
- data/lib/zip/pass_thru_compressor.rb +5 -3
- data/lib/zip/pass_thru_decompressor.rb +17 -23
- data/lib/zip/streamable_directory.rb +6 -4
- data/lib/zip/streamable_stream.rb +9 -5
- data/lib/zip/version.rb +4 -1
- data/lib/zip.rb +31 -5
- data/rubyzip.gemspec +39 -0
- data/samples/example.rb +9 -4
- data/samples/example_filesystem.rb +4 -3
- data/samples/example_recursive.rb +3 -1
- data/samples/gtk_ruby_zip.rb +23 -21
- data/samples/qtzip.rb +13 -12
- data/samples/write_simple.rb +3 -4
- data/samples/zipfind.rb +24 -22
- metadata +84 -28
- data/TODO +0 -15
- data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb
CHANGED
|
@@ -1,125 +1,116 @@
|
|
|
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, :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
|
|
86
|
+
initialize_cdir(path_or_io, buffer: buffer)
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
@restore_ownership = options[:restore_ownership]
|
|
112
|
-
@restore_permissions = options[:restore_permissions]
|
|
113
|
-
@restore_times = options[:restore_times]
|
|
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?
|
|
113
|
+
|
|
123
114
|
begin
|
|
124
115
|
yield zf
|
|
125
116
|
ensure
|
|
@@ -127,30 +118,32 @@ module Zip
|
|
|
127
118
|
end
|
|
128
119
|
end
|
|
129
120
|
|
|
130
|
-
# Same as #open. But outputs data to a buffer instead of a file
|
|
131
|
-
def add_buffer
|
|
132
|
-
io = ::StringIO.new('')
|
|
133
|
-
zf = ::Zip::File.new(io, true, true)
|
|
134
|
-
yield zf
|
|
135
|
-
zf.write_buffer(io)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
121
|
# Like #open, but reads zip archive contents from a String or open IO
|
|
139
122
|
# stream, and outputs data to a buffer.
|
|
140
123
|
# (This can be used to extract data from a
|
|
141
124
|
# downloaded zip archive without first saving it to disk.)
|
|
142
|
-
def open_buffer(io,
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
131
|
+
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
|
|
132
|
+
raise 'Zip::File.open_buffer expects a String or IO-like argument' \
|
|
133
|
+
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
|
145
134
|
end
|
|
146
135
|
|
|
147
|
-
io = ::StringIO.new(io) if io.
|
|
136
|
+
io = ::StringIO.new(io) if io.kind_of?(::String)
|
|
148
137
|
|
|
149
|
-
|
|
150
|
-
|
|
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)
|
|
151
144
|
|
|
152
|
-
zf = ::Zip::File.new(io, true, true, options)
|
|
153
145
|
return zf unless block_given?
|
|
146
|
+
|
|
154
147
|
yield zf
|
|
155
148
|
|
|
156
149
|
begin
|
|
@@ -166,93 +159,32 @@ module Zip
|
|
|
166
159
|
# whereas ZipInputStream jumps through the entire archive accessing the
|
|
167
160
|
# local entry headers (which contain the same information as the
|
|
168
161
|
# central directory).
|
|
169
|
-
def foreach(
|
|
170
|
-
open(
|
|
171
|
-
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def get_segment_size_for_split(segment_size)
|
|
176
|
-
if MIN_SEGMENT_SIZE > segment_size
|
|
177
|
-
MIN_SEGMENT_SIZE
|
|
178
|
-
elsif MAX_SEGMENT_SIZE < segment_size
|
|
179
|
-
MAX_SEGMENT_SIZE
|
|
180
|
-
else
|
|
181
|
-
segment_size
|
|
162
|
+
def foreach(zip_file_name, &block)
|
|
163
|
+
::Zip::File.open(zip_file_name) do |zip_file|
|
|
164
|
+
zip_file.each(&block)
|
|
182
165
|
end
|
|
183
166
|
end
|
|
184
167
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
end
|
|
190
|
-
partial_zip_file_name ||= zip_file_name
|
|
191
|
-
partial_zip_file_name
|
|
192
|
-
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
|
|
193
172
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def put_split_signature(szip_file, segment_size)
|
|
199
|
-
signature_packed = [SPLIT_SIGNATURE].pack('V')
|
|
200
|
-
szip_file << signature_packed
|
|
201
|
-
segment_size - signature_packed.size
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
# TODO: Make the code more understandable
|
|
206
|
-
#
|
|
207
|
-
def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
|
|
208
|
-
ssegment_size = zip_file_size - zip_file.pos
|
|
209
|
-
ssegment_size = segment_size if ssegment_size > segment_size
|
|
210
|
-
szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
|
|
211
|
-
::File.open(szip_file_name, 'wb') do |szip_file|
|
|
212
|
-
if szip_file_index == 1
|
|
213
|
-
ssegment_size = put_split_signature(szip_file, segment_size)
|
|
214
|
-
end
|
|
215
|
-
chunk_bytes = 0
|
|
216
|
-
until ssegment_size == chunk_bytes || zip_file.eof?
|
|
217
|
-
segment_bytes_left = ssegment_size - chunk_bytes
|
|
218
|
-
buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
|
|
219
|
-
chunk = zip_file.read(buffer_size)
|
|
220
|
-
chunk_bytes += buffer_size
|
|
221
|
-
szip_file << chunk
|
|
222
|
-
# Info for track splitting
|
|
223
|
-
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Splits an archive into parts with segment size
|
|
229
|
-
def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true, partial_zip_file_name = nil)
|
|
230
|
-
raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
|
|
231
|
-
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
|
|
232
|
-
zip_file_size = ::File.size(zip_file_name)
|
|
233
|
-
segment_size = get_segment_size_for_split(segment_size)
|
|
234
|
-
return if zip_file_size <= segment_size
|
|
235
|
-
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
|
|
236
|
-
# Checking for correct zip structure
|
|
237
|
-
open(zip_file_name) {}
|
|
238
|
-
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
|
|
239
|
-
szip_file_index = 0
|
|
240
|
-
::File.open(zip_file_name, 'rb') do |zip_file|
|
|
241
|
-
until zip_file.eof?
|
|
242
|
-
szip_file_index += 1
|
|
243
|
-
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)
|
|
244
176
|
end
|
|
177
|
+
else
|
|
178
|
+
cdir.count_entries(path_or_io)
|
|
245
179
|
end
|
|
246
|
-
::File.delete(zip_file_name) if delete_zip_file
|
|
247
|
-
szip_file_index
|
|
248
180
|
end
|
|
249
181
|
end
|
|
250
182
|
|
|
251
183
|
# Returns an input stream to the specified entry. If a block is passed
|
|
252
184
|
# the stream object is passed to the block and the stream is automatically
|
|
253
185
|
# closed afterwards just as with ruby's builtin File.open method.
|
|
254
|
-
def get_input_stream(entry, &
|
|
255
|
-
get_entry(entry).get_input_stream(&
|
|
186
|
+
def get_input_stream(entry, &a_proc)
|
|
187
|
+
get_entry(entry).get_input_stream(&a_proc)
|
|
256
188
|
end
|
|
257
189
|
|
|
258
190
|
# Returns an output stream to the specified entry. If entry is not an instance
|
|
@@ -260,21 +192,29 @@ module Zip
|
|
|
260
192
|
# specified. If a block is passed the stream object is passed to the block and
|
|
261
193
|
# the stream is automatically closed afterwards just as with ruby's builtin
|
|
262
194
|
# File.open method.
|
|
263
|
-
def get_output_stream(entry,
|
|
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)
|
|
264
199
|
new_entry =
|
|
265
200
|
if entry.kind_of?(Entry)
|
|
266
201
|
entry
|
|
267
202
|
else
|
|
268
|
-
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
|
+
)
|
|
269
209
|
end
|
|
270
210
|
if new_entry.directory?
|
|
271
211
|
raise ArgumentError,
|
|
272
212
|
"cannot open stream to directory entry - '#{new_entry}'"
|
|
273
213
|
end
|
|
274
|
-
new_entry.unix_perms =
|
|
214
|
+
new_entry.unix_perms = permissions
|
|
275
215
|
zip_streamable_entry = StreamableStream.new(new_entry)
|
|
276
|
-
@
|
|
277
|
-
zip_streamable_entry.get_output_stream(&
|
|
216
|
+
@cdir << zip_streamable_entry
|
|
217
|
+
zip_streamable_entry.get_output_stream(&a_proc)
|
|
278
218
|
end
|
|
279
219
|
|
|
280
220
|
# Returns the name of the zip archive
|
|
@@ -284,77 +224,92 @@ module Zip
|
|
|
284
224
|
|
|
285
225
|
# Returns a string containing the contents of the specified entry
|
|
286
226
|
def read(entry)
|
|
287
|
-
get_input_stream(entry
|
|
227
|
+
get_input_stream(entry, &:read)
|
|
288
228
|
end
|
|
289
229
|
|
|
290
230
|
# Convenience method for adding the contents of a file to the archive
|
|
291
231
|
def add(entry, src_path, &continue_on_exists_proc)
|
|
292
232
|
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
|
|
293
233
|
check_entry_exists(entry, continue_on_exists_proc, 'add')
|
|
294
|
-
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
|
|
295
242
|
new_entry.gather_fileinfo_from_srcpath(src_path)
|
|
296
|
-
|
|
297
|
-
@entry_set << new_entry
|
|
243
|
+
@cdir << new_entry
|
|
298
244
|
end
|
|
299
245
|
|
|
300
246
|
# Convenience method for adding the contents of a file to the archive
|
|
301
247
|
# in Stored format (uncompressed)
|
|
302
248
|
def add_stored(entry, src_path, &continue_on_exists_proc)
|
|
303
|
-
entry = ::Zip::Entry.new(
|
|
249
|
+
entry = ::Zip::Entry.new(
|
|
250
|
+
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
|
|
251
|
+
)
|
|
304
252
|
add(entry, src_path, &continue_on_exists_proc)
|
|
305
253
|
end
|
|
306
254
|
|
|
307
255
|
# Removes the specified entry.
|
|
308
256
|
def remove(entry)
|
|
309
|
-
@
|
|
257
|
+
@cdir.delete(get_entry(entry))
|
|
310
258
|
end
|
|
311
259
|
|
|
312
260
|
# Renames the specified entry.
|
|
313
261
|
def rename(entry, new_name, &continue_on_exists_proc)
|
|
314
|
-
|
|
262
|
+
found_entry = get_entry(entry)
|
|
315
263
|
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
|
|
316
|
-
@
|
|
317
|
-
|
|
318
|
-
@
|
|
264
|
+
@cdir.delete(found_entry)
|
|
265
|
+
found_entry.name = new_name
|
|
266
|
+
@cdir << found_entry
|
|
319
267
|
end
|
|
320
268
|
|
|
321
|
-
# Replaces the specified entry with the contents of
|
|
269
|
+
# Replaces the specified entry with the contents of src_path (from
|
|
322
270
|
# the file system).
|
|
323
|
-
def replace(entry,
|
|
324
|
-
check_file(
|
|
271
|
+
def replace(entry, src_path)
|
|
272
|
+
check_file(src_path)
|
|
325
273
|
remove(entry)
|
|
326
|
-
add(entry,
|
|
274
|
+
add(entry, src_path)
|
|
327
275
|
end
|
|
328
276
|
|
|
329
|
-
# Extracts entry to file
|
|
330
|
-
|
|
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)
|
|
331
283
|
block ||= proc { ::Zip.on_exists_proc }
|
|
332
284
|
found_entry = get_entry(entry)
|
|
333
|
-
found_entry.
|
|
285
|
+
entry_path ||= found_entry.name
|
|
286
|
+
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
|
|
334
287
|
end
|
|
335
288
|
|
|
336
289
|
# Commits changes that has been made since the previous commit to
|
|
337
290
|
# the zip archive.
|
|
338
291
|
def commit
|
|
339
|
-
return if name.
|
|
292
|
+
return if name.kind_of?(StringIO) || !commit_required?
|
|
293
|
+
|
|
340
294
|
on_success_replace do |tmp_file|
|
|
341
|
-
::Zip::OutputStream.open(tmp_file) do |zos|
|
|
342
|
-
@
|
|
295
|
+
::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
|
|
296
|
+
@cdir.each do |e|
|
|
343
297
|
e.write_to_zip_output_stream(zos)
|
|
344
|
-
e.dirty = false
|
|
345
298
|
e.clean_up
|
|
346
299
|
end
|
|
347
300
|
zos.comment = comment
|
|
348
301
|
end
|
|
349
302
|
true
|
|
350
303
|
end
|
|
351
|
-
|
|
304
|
+
initialize_cdir(@name)
|
|
352
305
|
end
|
|
353
306
|
|
|
354
307
|
# Write buffer write changes to buffer and return
|
|
355
|
-
def write_buffer(io = ::StringIO.new
|
|
356
|
-
|
|
357
|
-
|
|
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) }
|
|
358
313
|
zos.comment = comment
|
|
359
314
|
end
|
|
360
315
|
end
|
|
@@ -367,16 +322,19 @@ module Zip
|
|
|
367
322
|
# Returns true if any changes has been made to this archive since
|
|
368
323
|
# the previous commit
|
|
369
324
|
def commit_required?
|
|
370
|
-
@
|
|
371
|
-
|
|
325
|
+
return true if @create || @cdir.dirty?
|
|
326
|
+
|
|
327
|
+
@cdir.each do |e|
|
|
328
|
+
return true if e.dirty?
|
|
372
329
|
end
|
|
373
|
-
|
|
330
|
+
|
|
331
|
+
false
|
|
374
332
|
end
|
|
375
333
|
|
|
376
334
|
# Searches for entry with the specified name. Returns nil if
|
|
377
335
|
# no entry is found. See also get_entry
|
|
378
336
|
def find_entry(entry_name)
|
|
379
|
-
selected_entry = @
|
|
337
|
+
selected_entry = @cdir.find_entry(entry_name)
|
|
380
338
|
return if selected_entry.nil?
|
|
381
339
|
|
|
382
340
|
selected_entry.restore_ownership = @restore_ownership
|
|
@@ -385,11 +343,6 @@ module Zip
|
|
|
385
343
|
selected_entry
|
|
386
344
|
end
|
|
387
345
|
|
|
388
|
-
# Searches for entries given a glob
|
|
389
|
-
def glob(*args, &block)
|
|
390
|
-
@entry_set.glob(*args, &block)
|
|
391
|
-
end
|
|
392
|
-
|
|
393
346
|
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
|
394
347
|
# if no entry is found.
|
|
395
348
|
def get_entry(entry)
|
|
@@ -400,36 +353,55 @@ module Zip
|
|
|
400
353
|
end
|
|
401
354
|
|
|
402
355
|
# Creates a directory
|
|
403
|
-
def mkdir(
|
|
404
|
-
raise Errno::EEXIST, "File exists - #{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
356
|
+
def mkdir(entry_name, permission = 0o755)
|
|
357
|
+
raise Errno::EEXIST, "File exists - #{entry_name}" if find_entry(entry_name)
|
|
358
|
+
|
|
359
|
+
entry_name = entry_name.dup.to_s
|
|
360
|
+
entry_name << '/' unless entry_name.end_with?('/')
|
|
361
|
+
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
|
|
408
362
|
end
|
|
409
363
|
|
|
410
364
|
private
|
|
411
365
|
|
|
412
|
-
def
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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"
|
|
420
395
|
end
|
|
421
|
-
newEntry.directory? && srcPathIsDirectory
|
|
422
396
|
end
|
|
423
397
|
|
|
424
|
-
def check_entry_exists(
|
|
398
|
+
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
|
|
399
|
+
return unless @cdir.include?(entry_name)
|
|
400
|
+
|
|
425
401
|
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
else
|
|
430
|
-
raise ::Zip::EntryExistsError,
|
|
431
|
-
procedureName + " failed. Entry #{entryName} already exists"
|
|
432
|
-
end
|
|
402
|
+
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
|
|
403
|
+
|
|
404
|
+
remove get_entry(entry_name)
|
|
433
405
|
end
|
|
434
406
|
|
|
435
407
|
def check_file(path)
|
|
@@ -439,14 +411,12 @@ module Zip
|
|
|
439
411
|
def on_success_replace
|
|
440
412
|
dirname, basename = ::File.split(name)
|
|
441
413
|
::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
::File.chmod(@file_permissions, name) unless @create
|
|
446
|
-
end
|
|
447
|
-
ensure
|
|
448
|
-
::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
|
|
449
417
|
end
|
|
418
|
+
ensure
|
|
419
|
+
FileUtils.rm_f(tmp_filename)
|
|
450
420
|
end
|
|
451
421
|
end
|
|
452
422
|
end
|