omnizip 0.3.35 → 0.3.37
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 +67 -0
- data/docs/getting-started/basic-usage.adoc +14 -22
- data/docs/guides/advanced-features/encryption.adoc +16 -39
- data/docs/guides/advanced-features/index.adoc +16 -6
- data/docs/guides/advanced-features/parallel-processing.adoc +63 -165
- data/docs/guides/advanced-features/progress-tracking.adoc +65 -261
- data/docs/guides/advanced-features/streaming.adoc +43 -113
- data/docs/guides/compression-algorithms/bzip2.adoc +2 -1
- data/docs/guides/compression-algorithms/deflate.adoc +1 -7
- data/docs/guides/compression-algorithms/lzma.adoc +10 -14
- data/docs/guides/compression-algorithms/lzma2.adoc +34 -117
- data/docs/guides/compression-algorithms/ppmd.adoc +24 -27
- data/docs/guides/compression-algorithms/zstandard.adoc +85 -150
- data/docs/troubleshooting/index.adoc +7 -5
- data/lib/omnizip/algorithms/zstandard.rb +2 -3
- data/lib/omnizip/archive/builder.rb +48 -0
- data/lib/omnizip/archive/reader_session.rb +67 -0
- data/lib/omnizip/archive.rb +86 -0
- data/lib/omnizip/archive_handler.rb +1 -0
- data/lib/omnizip/archive_handlers/rar_handler.rb +50 -0
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +6 -2
- data/lib/omnizip/archive_handlers/tar_handler.rb +4 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +25 -2
- data/lib/omnizip/archive_handlers.rb +1 -0
- data/lib/omnizip/convenience.rb +91 -11
- data/lib/omnizip/formats/rar/block_parser.rb +8 -12
- data/lib/omnizip/formats/rar/constants.rb +3 -1
- data/lib/omnizip/formats/rar/header.rb +16 -6
- data/lib/omnizip/formats/rar/reader.rb +6 -2
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip.rb +1 -0
- metadata +6 -2
|
@@ -49,29 +49,33 @@ Zstandard (zstd) is a modern, fast compression algorithm developed by Facebook t
|
|
|
49
49
|
|
|
50
50
|
== Basic Usage
|
|
51
51
|
|
|
52
|
-
===
|
|
52
|
+
=== Compress a File (.zst)
|
|
53
53
|
|
|
54
54
|
[source,ruby]
|
|
55
55
|
----
|
|
56
|
-
#
|
|
57
|
-
Omnizip
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
# Writes a real Zstandard frame (validated by the system zstd CLI)
|
|
57
|
+
Omnizip.compress_file('logfile.log', 'logfile.log.zst')
|
|
58
|
+
|
|
59
|
+
# Compression level (generic 0-9 scale; values above 9 pass through
|
|
60
|
+
# as native zstd levels 1-22)
|
|
61
|
+
Omnizip.compress_file('data.bin', 'data.bin.zst', level: 9)
|
|
62
|
+
|
|
63
|
+
Omnizip.decompress_file('logfile.log.zst', 'logfile.log')
|
|
62
64
|
----
|
|
63
65
|
|
|
64
|
-
===
|
|
66
|
+
=== Zstandard Entries in ZIP
|
|
67
|
+
|
|
68
|
+
ZIP supports Zstandard as a per-entry method (method 93). Requires a
|
|
69
|
+
modern reader (e.g. 7-Zip 21+):
|
|
65
70
|
|
|
66
71
|
[source,ruby]
|
|
67
72
|
----
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
73
|
+
require 'omnizip/zip/output_stream'
|
|
74
|
+
|
|
75
|
+
Omnizip::Zip::OutputStream.open('fast.zip') do |zip|
|
|
76
|
+
zip.put_next_entry('data.bin', compression: :zstandard, level: 3)
|
|
77
|
+
zip.write(data)
|
|
78
|
+
end
|
|
75
79
|
----
|
|
76
80
|
|
|
77
81
|
== Compression Levels
|
|
@@ -106,109 +110,63 @@ Zstandard offers levels 1-22 with fine-grained control:
|
|
|
106
110
|
.Level Performance Example
|
|
107
111
|
[source,ruby]
|
|
108
112
|
----
|
|
109
|
-
# Compare different Zstandard levels
|
|
110
|
-
[1,
|
|
113
|
+
# Compare different Zstandard levels (native zstd scale)
|
|
114
|
+
[1, 12, 19].each do |level|
|
|
111
115
|
time = Benchmark.measure do
|
|
112
|
-
Omnizip.compress_file('data.bin', "zstd-#{level}.
|
|
113
|
-
compression: :zstandard,
|
|
114
|
-
level: level
|
|
115
|
-
)
|
|
116
|
+
Omnizip.compress_file('data.bin', "zstd-#{level}.zst", level: level)
|
|
116
117
|
end
|
|
117
118
|
|
|
118
|
-
size = File.size("zstd-#{level}.
|
|
119
|
+
size = File.size("zstd-#{level}.zst")
|
|
119
120
|
puts "Level #{level}: #{size / 1024} KB in #{time.real.round(3)}s"
|
|
120
121
|
end
|
|
121
|
-
|
|
122
|
-
# Output:
|
|
123
|
-
# Level 1: 145 KB in 0.052s (fastest)
|
|
124
|
-
# Level 3: 128 KB in 0.089s (default)
|
|
125
|
-
# Level 10: 105 KB in 0.425s (better compression)
|
|
126
|
-
# Level 19: 98 KB in 2.156s (approaching LZMA quality)
|
|
127
122
|
----
|
|
128
123
|
|
|
129
124
|
== Performance Comparison
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|Compress 100 MB
|
|
138
|
-
|10s
|
|
139
|
-
|3s
|
|
140
|
-
|3.3x faster
|
|
141
|
-
|
|
142
|
-
|Decompress 100 MB
|
|
143
|
-
|5s
|
|
144
|
-
|1.2s
|
|
145
|
-
|4.2x faster
|
|
146
|
-
|
|
147
|
-
|Compression Ratio
|
|
148
|
-
|68%
|
|
149
|
-
|72%
|
|
150
|
-
|4% better
|
|
151
|
-
|===
|
|
152
|
-
|
|
153
|
-
=== Speed vs LZMA
|
|
154
|
-
|
|
155
|
-
[cols="2,1,1,1"]
|
|
156
|
-
|===
|
|
157
|
-
|Operation |LZMA |Zstandard |Trade-off
|
|
158
|
-
|
|
159
|
-
|Compress 100 MB
|
|
160
|
-
|60s
|
|
161
|
-
|3s
|
|
162
|
-
|20x faster, slightly worse ratio
|
|
163
|
-
|
|
164
|
-
|Decompress 100 MB
|
|
165
|
-
|8s
|
|
166
|
-
|1.2s
|
|
167
|
-
|6.7x faster
|
|
168
|
-
|
|
169
|
-
|Compression Ratio
|
|
170
|
-
|78%
|
|
171
|
-
|72%
|
|
172
|
-
|LZMA ~6% better
|
|
173
|
-
|===
|
|
126
|
+
No benchmark tables here: Omnizip's Zstandard is a pure Ruby
|
|
127
|
+
implementation, so absolute throughput differs substantially from
|
|
128
|
+
the C library. The algorithmic trade-offs hold — faster than LZMA
|
|
129
|
+
per level, better ratio than Deflate — but measure on your own
|
|
130
|
+
workloads.
|
|
174
131
|
|
|
175
132
|
== Advanced Features
|
|
176
133
|
|
|
177
|
-
=== Dictionary
|
|
134
|
+
=== Dictionary Compression
|
|
178
135
|
|
|
179
|
-
|
|
136
|
+
A raw dictionary (shared prefix) dramatically improves ratios on
|
|
137
|
+
small, repetitive inputs. Dictionary *training* is not implemented;
|
|
138
|
+
prepare dictionary content yourself and load it with +from_raw+:
|
|
180
139
|
|
|
181
140
|
[source,ruby]
|
|
182
141
|
----
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
dictionary = Omnizip::Zstandard.train_dictionary(training_data, size: 100.kilobytes)
|
|
186
|
-
|
|
187
|
-
# Use trained dictionary for better compression
|
|
188
|
-
Omnizip.compress_file('data.json', 'output.7z',
|
|
189
|
-
compression: :zstandard,
|
|
190
|
-
level: 5,
|
|
191
|
-
dictionary: dictionary
|
|
142
|
+
dict = Omnizip::Algorithms::Zstandard::Dictionary.from_raw(
|
|
143
|
+
42, File.binread('shared-samples.bin')
|
|
192
144
|
)
|
|
193
145
|
|
|
194
|
-
|
|
146
|
+
zstd = Omnizip::Algorithms::Zstandard.new
|
|
147
|
+
File.open('data.json', 'rb') do |input|
|
|
148
|
+
File.open('data.json.zstd', 'wb') do |output|
|
|
149
|
+
zstd.compress_with_dict(input, output, dict)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
File.open('data.json.zstd', 'rb') do |input|
|
|
154
|
+
File.open('data.restored', 'wb') do |output|
|
|
155
|
+
zstd.decompress_with_dict(input, output, dict)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
195
158
|
----
|
|
196
159
|
|
|
197
|
-
=== Streaming
|
|
160
|
+
=== Streaming
|
|
198
161
|
|
|
199
|
-
|
|
162
|
+
The codec is stream-oriented:
|
|
200
163
|
|
|
201
164
|
[source,ruby]
|
|
202
165
|
----
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# Process log entries as they arrive
|
|
209
|
-
log_stream.each do |log_entry|
|
|
210
|
-
stream.put_next_entry("log_#{log_entry.id}.txt")
|
|
211
|
-
stream.write(log_entry.content)
|
|
166
|
+
zstd = Omnizip::AlgorithmRegistry.get(:zstandard).new
|
|
167
|
+
File.open('input.log', 'rb') do |input|
|
|
168
|
+
File.open('input.log.zst', 'wb') do |output|
|
|
169
|
+
zstd.compress(input, output, level: 3)
|
|
212
170
|
end
|
|
213
171
|
end
|
|
214
172
|
----
|
|
@@ -222,15 +180,8 @@ Perfect for compressing logs quickly:
|
|
|
222
180
|
[source,ruby]
|
|
223
181
|
----
|
|
224
182
|
# Compress today's logs rapidly
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Omnizip.compress_files(
|
|
228
|
-
log_files,
|
|
229
|
-
"logs_#{Date.today}.7z",
|
|
230
|
-
compression: :zstandard,
|
|
231
|
-
level: 5 # Fast with good compression
|
|
232
|
-
) do |progress|
|
|
233
|
-
puts "Compressing logs: #{progress.percentage}%"
|
|
183
|
+
Dir.glob('/var/log/app/*.log').each do |log|
|
|
184
|
+
Omnizip.compress_file(log, "#{log}.zst", level: 3)
|
|
234
185
|
end
|
|
235
186
|
----
|
|
236
187
|
|
|
@@ -240,47 +191,37 @@ Compress API responses on-the-fly:
|
|
|
240
191
|
|
|
241
192
|
[source,ruby]
|
|
242
193
|
----
|
|
194
|
+
require 'stringio'
|
|
195
|
+
|
|
243
196
|
class ExportController < ApplicationController
|
|
244
197
|
def download
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
archive.add(filename, content)
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
send_data compressed.string,
|
|
259
|
-
filename: 'export.7z',
|
|
260
|
-
type: 'application/x-7z-compressed'
|
|
198
|
+
data = generate_export_data # a String
|
|
199
|
+
|
|
200
|
+
zstd = Omnizip::AlgorithmRegistry.get(:zstandard).new
|
|
201
|
+
output = StringIO.new
|
|
202
|
+
zstd.compress(StringIO.new(data), output, level: 3)
|
|
203
|
+
|
|
204
|
+
send_data output.string,
|
|
205
|
+
filename: 'export.zst',
|
|
206
|
+
type: 'application/zstd'
|
|
261
207
|
end
|
|
262
208
|
end
|
|
263
209
|
----
|
|
264
210
|
|
|
265
211
|
=== Database Dumps
|
|
266
212
|
|
|
267
|
-
|
|
213
|
+
Stream a dump into a .zst frame:
|
|
268
214
|
|
|
269
215
|
[source,ruby]
|
|
270
216
|
----
|
|
271
|
-
# Stream database dump to compressed
|
|
217
|
+
# Stream database dump to compressed file
|
|
272
218
|
require 'open3'
|
|
273
219
|
|
|
274
|
-
Omnizip::
|
|
275
|
-
|
|
276
|
-
stream.level = 7
|
|
277
|
-
stream.put_next_entry('database.sql')
|
|
278
|
-
|
|
220
|
+
zstd = Omnizip::AlgorithmRegistry.get(:zstandard).new
|
|
221
|
+
File.open('db-backup.sql.zst', 'wb') do |output|
|
|
279
222
|
# Stream pg_dump output directly
|
|
280
|
-
Open3.popen3('pg_dump mydb') do |
|
|
281
|
-
|
|
282
|
-
stream.write(chunk)
|
|
283
|
-
end
|
|
223
|
+
Open3.popen3('pg_dump mydb') do |_stdin, stdout, _stderr, _wait_thr|
|
|
224
|
+
zstd.compress(stdout, output, level: 7)
|
|
284
225
|
end
|
|
285
226
|
end
|
|
286
227
|
----
|
|
@@ -324,34 +265,28 @@ Zstandard is memory efficient:
|
|
|
324
265
|
|
|
325
266
|
[source,bash]
|
|
326
267
|
----
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# Fast compression
|
|
333
|
-
$ omnizip archive create logs.7z logs/ \
|
|
334
|
-
--compression zstandard \
|
|
335
|
-
--level 1 \
|
|
336
|
-
--verbose
|
|
337
|
-
Algorithm: Zstandard, Level: 1
|
|
338
|
-
Compression speed: 185 MB/s
|
|
339
|
-
Original: 500 MB
|
|
340
|
-
Compressed: 145 MB (71% reduction)
|
|
341
|
-
Time: 2.7s
|
|
268
|
+
$ omnizip compress data.bin data.bin.zst --algorithm zstandard
|
|
269
|
+
|
|
270
|
+
# The output is a standard Zstandard frame, readable by the system
|
|
271
|
+
# zstd CLI
|
|
342
272
|
----
|
|
343
273
|
|
|
344
274
|
== Technical Details
|
|
345
275
|
|
|
346
276
|
**Zstandard Features**:
|
|
347
277
|
|
|
348
|
-
* **Fast Entropy Encoding**: Finite State Entropy (FSE)
|
|
278
|
+
* **Fast Entropy Encoding**: Finite State Entropy (FSE)
|
|
349
279
|
* **Adaptive Window**: Dynamically sized based on level
|
|
350
|
-
* **Dictionary Compression**:
|
|
351
|
-
|
|
352
|
-
* **
|
|
280
|
+
* **Dictionary Compression**: Raw-dictionary priming (entropy-table
|
|
281
|
+
preloading from RFC dictionaries is not implemented)
|
|
282
|
+
* **Frames**: Interoperable with the reference zstd CLI (verified
|
|
283
|
+
both directions)
|
|
284
|
+
|
|
285
|
+
**Implementation**: Pure Ruby, part of Omnizip. No native binding.
|
|
353
286
|
|
|
354
|
-
**
|
|
287
|
+
**Not available**: Zstandard is not offered as a 7z coder method;
|
|
288
|
+
in 7z archives use LZMA2/PPMd/BZip2. Multi-threading is not
|
|
289
|
+
implemented (single-threaded encoder).
|
|
355
290
|
|
|
356
291
|
== See Also
|
|
357
292
|
|
|
@@ -121,13 +121,15 @@ omnizip compress input.txt output.lzma --level 3
|
|
|
121
121
|
omnizip compress input.txt output.zst --algorithm zstandard
|
|
122
122
|
----
|
|
123
123
|
|
|
124
|
-
3.
|
|
124
|
+
3. Run independent archives in parallel processes (see the
|
|
125
|
+
link:../guides/advanced-features/parallel-processing.html[Parallel Processing guide]):
|
|
125
126
|
[source,ruby]
|
|
126
127
|
----
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
dirs = %w[data_a data_b]
|
|
129
|
+
pids = dirs.map do |dir|
|
|
130
|
+
Process.fork { Omnizip.compress_directory(dir, "#{dir}.7z") }
|
|
131
|
+
end
|
|
132
|
+
pids.each { |pid| Process.wait(pid) }
|
|
131
133
|
----
|
|
132
134
|
|
|
133
135
|
==== Slow Decompression
|
|
@@ -34,9 +34,8 @@ module Omnizip
|
|
|
34
34
|
# - Dictionary support for small data compression
|
|
35
35
|
# - Streaming and frame modes
|
|
36
36
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# the reference implementation and excellent performance.
|
|
37
|
+
# Pure Ruby implementation of the Zstandard format, interoperable
|
|
38
|
+
# with the reference zstd CLI in both directions.
|
|
40
39
|
#
|
|
41
40
|
# Zstandard is particularly effective for:
|
|
42
41
|
# - Real-time compression needs
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Archive
|
|
5
|
+
# Block argument of +Archive.create+. Translates the documented
|
|
6
|
+
# +add_file+/+add_directory+/+add_data+ vocabulary onto the
|
|
7
|
+
# handler proxy's +add(name, source_path)+ / +add_data+ calls.
|
|
8
|
+
class Builder
|
|
9
|
+
def initialize(proxy)
|
|
10
|
+
@proxy = proxy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Add a file from disk, optionally under a different archive
|
|
14
|
+
# path: +add_file('report.pdf', 'reports/report.pdf')+.
|
|
15
|
+
def add_file(file_path, archive_path = nil)
|
|
16
|
+
@proxy.add(archive_path || ::File.basename(file_path), file_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Add a directory tree under its own name:
|
|
20
|
+
# +add_directory('photos/')+ stores +photos/x.jpg+.
|
|
21
|
+
def add_directory(dir_path)
|
|
22
|
+
prefix = ::File.basename(::File.expand_path(dir_path))
|
|
23
|
+
@proxy.add("#{prefix}/")
|
|
24
|
+
walk(dir_path, prefix)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add in-memory content at +archive_path+.
|
|
28
|
+
def add_data(archive_path, data)
|
|
29
|
+
@proxy.add_data(archive_path, data)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def walk(dir, prefix)
|
|
35
|
+
::Dir.children(dir).sort.each do |child|
|
|
36
|
+
full = ::File.join(dir, child)
|
|
37
|
+
name = "#{prefix}/#{child}"
|
|
38
|
+
if ::File.directory?(full)
|
|
39
|
+
@proxy.add("#{name}/")
|
|
40
|
+
walk(full, name)
|
|
41
|
+
else
|
|
42
|
+
@proxy.add(name, full)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module Archive
|
|
7
|
+
# Block argument (and return value) of +Archive.open+. Delegates
|
|
8
|
+
# per operation to the resolved ArchiveHandler, which opens and
|
|
9
|
+
# closes the underlying reader on every call — the session itself
|
|
10
|
+
# holds no resources.
|
|
11
|
+
class ReaderSession
|
|
12
|
+
def initialize(handler, path, options = {})
|
|
13
|
+
@handler = handler
|
|
14
|
+
@path = path
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Archive metadata records (see Archive::Entry).
|
|
19
|
+
def entries
|
|
20
|
+
@handler.list(@path, details: true, **@options).map do |h|
|
|
21
|
+
Entry.new(name: h[:name], size: h[:size], directory: h[:directory],
|
|
22
|
+
mtime: h[:mtime] || h[:time])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each_entry(&block)
|
|
27
|
+
entries.each(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Contents of a single entry, without extraction.
|
|
31
|
+
def read(entry_name)
|
|
32
|
+
@handler.read_entry(@path, entry_name, **@options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Extract one entry to an explicit destination path.
|
|
36
|
+
def extract(entry_name, dest_path)
|
|
37
|
+
entry = entries.find { |e| e.name == entry_name }
|
|
38
|
+
unless entry
|
|
39
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if entry.directory?
|
|
43
|
+
::FileUtils.mkdir_p(dest_path)
|
|
44
|
+
return dest_path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
::FileUtils.mkdir_p(::File.dirname(dest_path))
|
|
48
|
+
::File.binwrite(dest_path, read(entry_name))
|
|
49
|
+
dest_path
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_all(output_dir)
|
|
53
|
+
@handler.extract_to(@path, output_dir, **@options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Extract every entry whose name matches +pattern+ under
|
|
57
|
+
# +output_dir+, preserving archive-relative paths.
|
|
58
|
+
def extract_matching(pattern, output_dir)
|
|
59
|
+
::FileUtils.mkdir_p(output_dir)
|
|
60
|
+
entries.select do |e|
|
|
61
|
+
!e.directory? && ::File.fnmatch?(pattern, e.name,
|
|
62
|
+
::File::FNM_PATHNAME)
|
|
63
|
+
end.map { |e| extract(e.name, ::File.join(output_dir, e.name)) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
# High-level block-style archive facade over the registered
|
|
5
|
+
# ArchiveHandlers. This is the API the guides document:
|
|
6
|
+
#
|
|
7
|
+
# Omnizip::Archive.create('backup.7z', format: :seven_zip,
|
|
8
|
+
# level: 9) do |archive|
|
|
9
|
+
# archive.add_file('report.pdf', 'reports/report.pdf')
|
|
10
|
+
# archive.add_directory('photos/')
|
|
11
|
+
# archive.add_data('notes/readme.txt', "generated\n")
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Omnizip::Archive.open('backup.7z') do |archive|
|
|
15
|
+
# archive.entries.each { |e| puts "#{e.name} (#{e.size})" }
|
|
16
|
+
# archive.read('notes/readme.txt')
|
|
17
|
+
# archive.extract_all('output/')
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# The format is resolved from the output path's extension when
|
|
21
|
+
# +format:+ is not given explicitly, exactly like the convenience
|
|
22
|
+
# methods. Passwords are supported for 7z only (ZIP writing and
|
|
23
|
+
# reading have no encryption support); asking for one elsewhere
|
|
24
|
+
# raises rather than silently producing an unprotected archive.
|
|
25
|
+
module Archive
|
|
26
|
+
autoload :Builder, "omnizip/archive/builder"
|
|
27
|
+
autoload :ReaderSession, "omnizip/archive/reader_session"
|
|
28
|
+
|
|
29
|
+
# Metadata record yielded by ReaderSession#entries. The +size+
|
|
30
|
+
# member intentionally shadows Struct#size: the documented
|
|
31
|
+
# contract is entry size in bytes, not the member count.
|
|
32
|
+
# rubocop:disable-next Lint/StructNewOverride -- documented byte size
|
|
33
|
+
Entry = Struct.new(:name, :size, :directory, :mtime, keyword_init: true) do
|
|
34
|
+
def directory?
|
|
35
|
+
directory
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
PASSWORD_FORMATS = [:seven_zip].freeze
|
|
40
|
+
private_constant :PASSWORD_FORMATS
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Create an archive, yielding a Builder that translates the
|
|
44
|
+
# documented +add_file+/+add_directory+/+add_data+ calls onto
|
|
45
|
+
# the resolved handler. Returns the archive path.
|
|
46
|
+
def create(path, format: Convenience::DEFAULT_FORMAT, **options, &block)
|
|
47
|
+
resolved = Omnizip.resolve_archive_format(path, format)
|
|
48
|
+
check_password_support!(resolved, options[:password])
|
|
49
|
+
|
|
50
|
+
Omnizip::ArchiveHandler.for(resolved).create(path, **options) do |proxy|
|
|
51
|
+
block&.call(Builder.new(proxy))
|
|
52
|
+
end
|
|
53
|
+
path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Open an archive for reading, yielding a ReaderSession. Without
|
|
57
|
+
# a block, returns the session. An explicit +password:+ wins
|
|
58
|
+
# over ENV['OMNIZIP_PASSWORD'].
|
|
59
|
+
def open(path, password: nil, **options, &block)
|
|
60
|
+
resolved = Omnizip.resolve_archive_format(
|
|
61
|
+
path, Convenience::DEFAULT_FORMAT, writing: false
|
|
62
|
+
)
|
|
63
|
+
pw = password || ENV.fetch("OMNIZIP_PASSWORD", nil)
|
|
64
|
+
check_password_support!(resolved, pw)
|
|
65
|
+
|
|
66
|
+
options[:password] = pw if pw
|
|
67
|
+
session = ReaderSession.new(
|
|
68
|
+
Omnizip::ArchiveHandler.for(resolved), path, options
|
|
69
|
+
)
|
|
70
|
+
return session unless block
|
|
71
|
+
|
|
72
|
+
yield session
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def check_password_support!(resolved, password)
|
|
78
|
+
return unless password && !PASSWORD_FORMATS.include?(resolved)
|
|
79
|
+
|
|
80
|
+
raise Omnizip::UnsupportedFormatError,
|
|
81
|
+
"password is only supported for #{PASSWORD_FORMATS.join(', ')}; " \
|
|
82
|
+
"#{resolved.inspect} has no encryption support"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -70,6 +70,7 @@ module Omnizip
|
|
|
70
70
|
zip: -> { Omnizip::ArchiveHandlers::ZipHandler },
|
|
71
71
|
tar: -> { Omnizip::ArchiveHandlers::TarHandler },
|
|
72
72
|
seven_zip: -> { Omnizip::ArchiveHandlers::SevenZipHandler },
|
|
73
|
+
rar: -> { Omnizip::ArchiveHandlers::RarHandler },
|
|
73
74
|
}.freeze
|
|
74
75
|
private_constant :LAZY_LOAD_TRIGGERS
|
|
75
76
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the RAR format. RAR compression is
|
|
8
|
+
# patented software (see link:README[RAR write support]); this
|
|
9
|
+
# handler exposes extraction/listing/reading only, so the
|
|
10
|
+
# convenience API and the Archive facade can operate on existing
|
|
11
|
+
# archives truthfully instead of raising on every operation.
|
|
12
|
+
class RarHandler
|
|
13
|
+
def create(_path, **_options)
|
|
14
|
+
raise Omnizip::UnsupportedFormatError,
|
|
15
|
+
"RAR archives cannot be created (patented format); " \
|
|
16
|
+
"Omnizip reads RAR archives only"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extract_to(path, output_dir, password: nil, **_)
|
|
20
|
+
Omnizip::Formats::Rar::Decompressor.extract(path, output_dir,
|
|
21
|
+
password: password)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list(path, details: false, **_)
|
|
25
|
+
reader = Omnizip::Formats::Rar::Reader.new(path).open
|
|
26
|
+
entries = reader.list_files
|
|
27
|
+
if details
|
|
28
|
+
entries.map do |e|
|
|
29
|
+
{ name: e.name, size: e.size, directory: e.is_dir,
|
|
30
|
+
mtime: e.mtime }
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
entries.map(&:name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read_entry(path, entry_name, password: nil, **_)
|
|
38
|
+
Dir.mktmpdir("omnizip-rar-entry") do |dir|
|
|
39
|
+
dest = File.join(dir, "entry")
|
|
40
|
+
Omnizip::Formats::Rar::Decompressor.extract_entry(
|
|
41
|
+
path, entry_name, dest, password: password
|
|
42
|
+
)
|
|
43
|
+
File.binread(dest)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Omnizip::ArchiveHandler.register(:rar, Omnizip::ArchiveHandlers::RarHandler.new)
|
|
@@ -8,8 +8,8 @@ module Omnizip
|
|
|
8
8
|
# Creates a .7z archive. The yielded proxy only supports
|
|
9
9
|
# +add(name, source_path)+ for files — directory entries are not
|
|
10
10
|
# part of the single-file convenience flow.
|
|
11
|
-
def create(path, &block)
|
|
12
|
-
Omnizip::Formats::SevenZip.create(path) do |writer|
|
|
11
|
+
def create(path, **options, &block)
|
|
12
|
+
Omnizip::Formats::SevenZip.create(path, options) do |writer|
|
|
13
13
|
block&.call(FileProxy.new(writer))
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -73,6 +73,10 @@ module Omnizip
|
|
|
73
73
|
|
|
74
74
|
@writer.add_file(source_path, name)
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
def add_data(name, data)
|
|
78
|
+
@writer.add_data(name, data)
|
|
79
|
+
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
78
82
|
end
|