omnizip 0.3.53 → 0.3.55
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 +65 -0
- data/lib/omnizip/algorithms/lzma/dictionary.rb +2 -2
- data/lib/omnizip/algorithms/lzma/lzma_alone_decoder.rb +7 -7
- data/lib/omnizip/algorithms/lzma/xz_buffered_range_encoder.rb +4 -4
- data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +7 -7
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +8 -8
- data/lib/omnizip/buffer/memory_archive.rb +55 -60
- data/lib/omnizip/buffer/memory_extractor.rb +25 -27
- data/lib/omnizip/buffer.rb +15 -8
- data/lib/omnizip/commands/metadata_command.rb +14 -6
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +2 -2
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +5 -5
- data/lib/omnizip/formats/seven_zip/encrypted_header.rb +2 -2
- data/lib/omnizip/formats/seven_zip/file_collector.rb +1 -1
- data/lib/omnizip/formats/seven_zip/header.rb +4 -4
- data/lib/omnizip/formats/seven_zip/header_encryptor.rb +2 -2
- data/lib/omnizip/formats/seven_zip/parser.rb +8 -8
- data/lib/omnizip/formats/seven_zip/reader.rb +14 -19
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +5 -5
- data/lib/omnizip/formats/seven_zip/stream_compressor.rb +1 -1
- data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +5 -5
- data/lib/omnizip/formats/seven_zip/writer.rb +1 -1
- data/lib/omnizip/formats/zip/writer.rb +13 -6
- data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +5 -5
- data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma/state_machine.rb +1 -1
- data/lib/omnizip/metadata/metadata_registry.rb +9 -2
- data/lib/omnizip/parallel/engine.rb +9 -5
- data/lib/omnizip/parallel/job_scheduler.rb +5 -1
- data/lib/omnizip/parallel/parallel_extractor.rb +62 -151
- data/lib/omnizip/parallel/thread_pool.rb +60 -0
- data/lib/omnizip/parallel/worker_pool.rb +5 -1
- data/lib/omnizip/parallel.rb +1 -0
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip.rb +3 -0
- metadata +2 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "fractor"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
|
|
6
5
|
module Omnizip
|
|
@@ -21,101 +20,6 @@ module Omnizip
|
|
|
21
20
|
# extractor = Omnizip::Parallel::ParallelExtractor.new(options)
|
|
22
21
|
# extractor.extract('backup.zip', 'output/')
|
|
23
22
|
class ParallelExtractor
|
|
24
|
-
# Fractor Work class for extraction jobs
|
|
25
|
-
class ExtractionWork < Fractor::Work
|
|
26
|
-
def initialize(entry:, archive_path:, dest_dir:)
|
|
27
|
-
super({
|
|
28
|
-
entry: entry,
|
|
29
|
-
archive_path: archive_path,
|
|
30
|
-
dest_dir: dest_dir,
|
|
31
|
-
})
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def entry
|
|
35
|
-
input[:entry]
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def archive_path
|
|
39
|
-
input[:archive_path]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def dest_dir
|
|
43
|
-
input[:dest_dir]
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Fractor Worker class for extraction
|
|
48
|
-
class ExtractionWorker < Fractor::Worker
|
|
49
|
-
def process(work)
|
|
50
|
-
entry = work.entry
|
|
51
|
-
archive_path = work.archive_path
|
|
52
|
-
dest_dir = work.dest_dir
|
|
53
|
-
|
|
54
|
-
# Read and decompress entry data
|
|
55
|
-
data = read_entry_data(archive_path, entry)
|
|
56
|
-
|
|
57
|
-
# Determine destination path
|
|
58
|
-
dest_path = ::File.join(dest_dir, entry.name)
|
|
59
|
-
|
|
60
|
-
# Return result
|
|
61
|
-
Fractor::WorkResult.new(
|
|
62
|
-
result: {
|
|
63
|
-
entry_name: entry.name,
|
|
64
|
-
dest_path: dest_path,
|
|
65
|
-
data: data,
|
|
66
|
-
directory: entry.directory?,
|
|
67
|
-
unix_perms: safe_unix_perms(entry),
|
|
68
|
-
},
|
|
69
|
-
work: work,
|
|
70
|
-
)
|
|
71
|
-
rescue StandardError => e
|
|
72
|
-
Fractor::WorkResult.new(
|
|
73
|
-
error: e,
|
|
74
|
-
work: work,
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
def read_entry_data(archive_path, entry)
|
|
81
|
-
return "" if entry.directory?
|
|
82
|
-
|
|
83
|
-
# Open archive and extract entry
|
|
84
|
-
reader = Omnizip::Formats::Zip::Reader.new(archive_path)
|
|
85
|
-
reader.read
|
|
86
|
-
|
|
87
|
-
::File.open(archive_path, "rb") do |io|
|
|
88
|
-
# Find the entry in reader
|
|
89
|
-
reader_entry = reader.entries.find { |e| e.filename == entry.name }
|
|
90
|
-
raise Errno::ENOENT, "Entry not found in archive: #{entry.name}" unless reader_entry
|
|
91
|
-
|
|
92
|
-
# Seek to entry data
|
|
93
|
-
io.seek(reader_entry.local_header_offset, ::IO::SEEK_SET)
|
|
94
|
-
|
|
95
|
-
# Read and parse local file header
|
|
96
|
-
fixed_header = io.read(30)
|
|
97
|
-
return "" unless fixed_header && fixed_header.size == 30
|
|
98
|
-
|
|
99
|
-
_signature, _version, _flags, _method, _time, _date, _crc32,
|
|
100
|
-
_comp_size, _uncomp_size, filename_length, extra_length = fixed_header.unpack("VvvvvvVVVvv")
|
|
101
|
-
|
|
102
|
-
# Skip filename and extra field
|
|
103
|
-
io.read(filename_length + extra_length)
|
|
104
|
-
|
|
105
|
-
# Read compressed data
|
|
106
|
-
compressed_data = io.read(reader_entry.compressed_size)
|
|
107
|
-
return "" unless compressed_data
|
|
108
|
-
|
|
109
|
-
# Decompress
|
|
110
|
-
reader.decompress(
|
|
111
|
-
compressed_data,
|
|
112
|
-
reader_entry.compression_method,
|
|
113
|
-
reader_entry.uncompressed_size,
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
23
|
# @return [Omnizip::Models::ParallelOptions] parallel options
|
|
120
24
|
attr_reader :options
|
|
121
25
|
|
|
@@ -179,41 +83,41 @@ module Omnizip
|
|
|
179
83
|
|
|
180
84
|
# Schedule jobs
|
|
181
85
|
entries.each do |entry|
|
|
182
|
-
file_size = safe_entry_size(entry)
|
|
183
|
-
|
|
184
86
|
job_queue.push_with_size(
|
|
185
|
-
file: entry.
|
|
186
|
-
size:
|
|
187
|
-
data: {
|
|
87
|
+
file: entry.filename,
|
|
88
|
+
size: safe_entry_size(entry),
|
|
89
|
+
data: { entry_name: entry.filename },
|
|
188
90
|
)
|
|
189
91
|
end
|
|
190
92
|
|
|
191
|
-
# Create work items from jobs
|
|
93
|
+
# Create work items from jobs, largest first
|
|
192
94
|
work_items = []
|
|
193
95
|
until job_queue.empty?
|
|
194
96
|
job = job_queue.pop(timeout: 0.1)
|
|
195
97
|
break unless job
|
|
196
98
|
|
|
197
|
-
work_items <<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
99
|
+
work_items << job.data[:entry_name]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Threads, not Ractors: Omnizip's autoloads and Proc-holding
|
|
103
|
+
# codec constants cannot cross Ractor boundaries on Ruby <= 3.3
|
|
104
|
+
pool = ThreadPool.new(size: @options.threads)
|
|
105
|
+
raw_results, raw_errors = pool.map(work_items) do |entry_name|
|
|
106
|
+
process_entry(archive, dest, entry_name)
|
|
202
107
|
end
|
|
203
108
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
errors = engine.run(work_items) { |r| results << r }
|
|
209
|
-
|
|
210
|
-
# Handle errors
|
|
211
|
-
unless errors.empty?
|
|
212
|
-
error_msgs = errors.map do |e|
|
|
213
|
-
"#{e.work&.entry&.name}: #{e.error}"
|
|
214
|
-
end.join("\n")
|
|
215
|
-
raise Omnizip::ExtractionError, "Extraction errors:\n#{error_msgs}"
|
|
109
|
+
# Handle errors (raw_errors holds nil for every item that
|
|
110
|
+
# succeeded — zip+compact would keep those pairs)
|
|
111
|
+
failure_pairs = raw_errors.each_with_index.filter_map do |error, index|
|
|
112
|
+
[work_items[index], error] if error
|
|
216
113
|
end
|
|
114
|
+
unless failure_pairs.empty?
|
|
115
|
+
error_msgs = failure_pairs.map { |name, e| "#{name}: #{e.message}" }
|
|
116
|
+
.join("\n")
|
|
117
|
+
raise Omnizip::DecompressionError, "Extraction errors:\n#{error_msgs}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
results = raw_results
|
|
217
121
|
|
|
218
122
|
# Write files to disk (thread-safe)
|
|
219
123
|
extracted_paths = write_extracted_files(results, overwrite: overwrite)
|
|
@@ -242,32 +146,37 @@ module Omnizip
|
|
|
242
146
|
|
|
243
147
|
private
|
|
244
148
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
149
|
+
# Decompress one entry in a worker thread. Each worker opens
|
|
150
|
+
# the archive independently — its own file handle per thread.
|
|
151
|
+
def process_entry(archive_path, dest_dir, entry_name)
|
|
152
|
+
entry = Omnizip::Formats::Zip::Reader.new(archive_path)
|
|
153
|
+
.entries.find { |e| e.filename == entry_name }
|
|
154
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
entry_name: entry_name,
|
|
158
|
+
dest_path: ::File.join(dest_dir, entry_name),
|
|
159
|
+
data: entry.directory? ? "" : read_entry_data(archive_path, entry_name),
|
|
160
|
+
directory: entry.directory?,
|
|
161
|
+
unix_perms: entry.unix_permissions.to_i,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def read_entry_data(archive_path, entry_name)
|
|
166
|
+
Omnizip::Formats::Zip::Reader.new(archive_path)
|
|
167
|
+
.read_entry(entry_name)
|
|
249
168
|
end
|
|
250
169
|
|
|
251
170
|
def safe_entry_size(entry)
|
|
252
|
-
entry.
|
|
253
|
-
rescue NoMethodError
|
|
254
|
-
0
|
|
171
|
+
entry.uncompressed_size.to_i
|
|
255
172
|
end
|
|
256
173
|
|
|
257
|
-
# Read archive entries
|
|
174
|
+
# Read archive entries through the native reader
|
|
258
175
|
#
|
|
259
176
|
# @param archive_path [String] archive path
|
|
260
|
-
# @return [Array<
|
|
177
|
+
# @return [Array<CentralDirectoryHeader>] array of entries
|
|
261
178
|
def read_archive_entries(archive_path)
|
|
262
|
-
entries
|
|
263
|
-
|
|
264
|
-
Omnizip::Zip::File.open(archive_path) do |zip|
|
|
265
|
-
zip.each do |entry|
|
|
266
|
-
entries << entry
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
entries
|
|
179
|
+
Omnizip::Formats::Zip::Reader.new(archive_path).entries
|
|
271
180
|
end
|
|
272
181
|
|
|
273
182
|
# Write extracted files to disk
|
|
@@ -278,34 +187,36 @@ module Omnizip
|
|
|
278
187
|
def write_extracted_files(results, overwrite: false)
|
|
279
188
|
extracted_paths = []
|
|
280
189
|
|
|
281
|
-
results.each do |
|
|
282
|
-
result = work_result.result
|
|
190
|
+
results.each do |result|
|
|
283
191
|
next unless result
|
|
284
192
|
|
|
285
193
|
dest_path = result[:dest_path]
|
|
286
194
|
|
|
287
195
|
# Thread-safe file writing
|
|
288
196
|
@write_mutex.synchronize do
|
|
197
|
+
if result[:directory]
|
|
198
|
+
# mkdir_p is idempotent — a file entry may already have
|
|
199
|
+
# created this directory as its parent
|
|
200
|
+
FileUtils.mkdir_p(dest_path)
|
|
201
|
+
extracted_paths << dest_path
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
289
205
|
# Check if file exists
|
|
290
206
|
if ::File.exist?(dest_path) && !overwrite
|
|
291
207
|
raise Errno::EEXIST, "File exists: #{dest_path}"
|
|
292
208
|
end
|
|
293
209
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
FileUtils.mkdir_p(dest_path)
|
|
297
|
-
else
|
|
298
|
-
FileUtils.mkdir_p(::File.dirname(dest_path))
|
|
299
|
-
::File.binwrite(dest_path, result[:data])
|
|
210
|
+
FileUtils.mkdir_p(::File.dirname(dest_path))
|
|
211
|
+
::File.binwrite(dest_path, result[:data])
|
|
300
212
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
@stats[:bytes_extracted] += result[:data].bytesize
|
|
213
|
+
# Set permissions if Unix
|
|
214
|
+
if result[:unix_perms].positive?
|
|
215
|
+
::File.chmod(result[:unix_perms] & 0o777, dest_path)
|
|
307
216
|
end
|
|
308
217
|
|
|
218
|
+
@stats[:bytes_extracted] += result[:data].bytesize
|
|
219
|
+
|
|
309
220
|
extracted_paths << dest_path
|
|
310
221
|
end
|
|
311
222
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Parallel
|
|
5
|
+
# Bounded thread pool for library work.
|
|
6
|
+
#
|
|
7
|
+
# Unlike the Fractor pool, threads share the VM state — Ractor
|
|
8
|
+
# workers cannot trigger Omnizip's autoloads or touch Proc-holding
|
|
9
|
+
# codec constants on Ruby <= 3.3 ("require by autoload on
|
|
10
|
+
# non-main Ractor is not supported"). Decompression itself still
|
|
11
|
+
# parallelizes: the zlib/bzip2 C extensions release the GVL.
|
|
12
|
+
class ThreadPool
|
|
13
|
+
# @return [Integer] number of worker threads
|
|
14
|
+
attr_reader :size
|
|
15
|
+
|
|
16
|
+
# Initialize the pool
|
|
17
|
+
#
|
|
18
|
+
# @param size [Integer] worker thread count (at least 1)
|
|
19
|
+
def initialize(size:)
|
|
20
|
+
@size = [size.to_i, 1].max
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Run +block+ for every item, at most +size+ at a time, and
|
|
24
|
+
# return a pair of parallel arrays: results (nil where an item
|
|
25
|
+
# raised) and errors (nil where it succeeded). Item order is
|
|
26
|
+
# preserved in both arrays.
|
|
27
|
+
#
|
|
28
|
+
# @param items [Array] work items
|
|
29
|
+
# @yieldparam item [Object] one work item
|
|
30
|
+
# @return [Array<Array, Array>]
|
|
31
|
+
def map(items)
|
|
32
|
+
results = Array.new(items.size)
|
|
33
|
+
errors = Array.new(items.size)
|
|
34
|
+
queue = Queue.new
|
|
35
|
+
items.each_with_index { |item, index| queue << [item, index] }
|
|
36
|
+
|
|
37
|
+
threads = Array.new(@size) do
|
|
38
|
+
Thread.new do
|
|
39
|
+
loop do
|
|
40
|
+
begin
|
|
41
|
+
item, index = queue.pop(true)
|
|
42
|
+
rescue ThreadError
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
results[index] = yield(item)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
errors[index] = e
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
threads.each(&:join)
|
|
55
|
+
|
|
56
|
+
[results, errors]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -132,8 +132,12 @@ module Omnizip
|
|
|
132
132
|
def shutdown(timeout: 30)
|
|
133
133
|
return unless @running
|
|
134
134
|
|
|
135
|
+
# Stop in BOTH modes: batch worker Ractors otherwise linger
|
|
136
|
+
# in Ractor.receive for the life of the process, burning CPU
|
|
137
|
+
# after the batch completes
|
|
138
|
+
@supervisor&.stop
|
|
139
|
+
|
|
135
140
|
if @continuous
|
|
136
|
-
@supervisor.stop
|
|
137
141
|
@supervisor_thread&.join(timeout)
|
|
138
142
|
end
|
|
139
143
|
|
data/lib/omnizip/parallel.rb
CHANGED
|
@@ -29,6 +29,7 @@ module Omnizip
|
|
|
29
29
|
autoload :Engine, "omnizip/parallel/engine"
|
|
30
30
|
autoload :ParallelCompressor, "omnizip/parallel/parallel_compressor"
|
|
31
31
|
autoload :ParallelExtractor, "omnizip/parallel/parallel_extractor"
|
|
32
|
+
autoload :ThreadPool, "omnizip/parallel/thread_pool"
|
|
32
33
|
|
|
33
34
|
class << self
|
|
34
35
|
# Global configuration
|
data/lib/omnizip/version.rb
CHANGED
data/lib/omnizip.rb
CHANGED
|
@@ -85,6 +85,9 @@ module Omnizip
|
|
|
85
85
|
autoload :ArchiveHandlers, "omnizip/archive_handlers"
|
|
86
86
|
autoload :Archive, "omnizip/archive"
|
|
87
87
|
autoload :Password, "omnizip/password"
|
|
88
|
+
# The class lives at the top level of omnizip/password.rb; without
|
|
89
|
+
# this entry, referencing it does not trigger the Password autoload
|
|
90
|
+
autoload :PasswordError, "omnizip/password"
|
|
88
91
|
autoload :Convenience, "omnizip/convenience"
|
|
89
92
|
end
|
|
90
93
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omnizip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.55
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -650,6 +650,7 @@ files:
|
|
|
650
650
|
- lib/omnizip/parallel/job_scheduler.rb
|
|
651
651
|
- lib/omnizip/parallel/parallel_compressor.rb
|
|
652
652
|
- lib/omnizip/parallel/parallel_extractor.rb
|
|
653
|
+
- lib/omnizip/parallel/thread_pool.rb
|
|
653
654
|
- lib/omnizip/parallel/worker_pool.rb
|
|
654
655
|
- lib/omnizip/parity.rb
|
|
655
656
|
- lib/omnizip/parity/chunked_block_processor.rb
|