omnizip 0.3.47 → 0.3.48
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 +10 -0
- data/lib/omnizip/archive.rb +1 -1
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +5 -7
- data/lib/omnizip/buffer/seven_zip_bridge.rb +5 -1
- data/lib/omnizip/commands/archive_create_command.rb +88 -211
- data/lib/omnizip/formats/rar/writer.rb +3 -1
- data/lib/omnizip/formats/seven_zip/writer.rb +3 -1
- data/lib/omnizip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ff2a922c26dc80694f6167b0d46e16d28fe34fa9a0cdbcea12f6f5dc57085b6
|
|
4
|
+
data.tar.gz: f260edbb81b37ba0824fb9a896fc1668a60d3e74ffeba44821670d4304dd065d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b47b5b5702166234e3c5e5cc132f3f944837904a9f4ce91a560e6c4207de329ec8d1e5ae01028e1b872c4dfbf5fda8b0ffee981ac9698cf500bc114d0a0ae65
|
|
7
|
+
data.tar.gz: e788bec8df533ea0417eb8db6e762a89ac17908e676558132eb127e1aa18af4c64bdc6da2151d50d772a4754c4a81ced191fa4b1b750fb6df27683d031ad237b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.47] - 2026-08-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `.rar` is a writable extension route: the RAR handler gained
|
|
14
|
+
create in 0.3.46, but the route table still classified `.rar` as
|
|
15
|
+
read-only — `Archive.create('a.rar')` without an explicit format
|
|
16
|
+
kept raising "cannot be written by the convenience API" while
|
|
17
|
+
`Archive.create(fmt: :rar)` worked. The route table matches the
|
|
18
|
+
handler now; `.cpio`/`.iso` stay read-only.
|
|
19
|
+
|
|
10
20
|
## [0.3.46] - 2026-08-31
|
|
11
21
|
|
|
12
22
|
### Added
|
data/lib/omnizip/archive.rb
CHANGED
|
@@ -69,14 +69,12 @@ module Omnizip
|
|
|
69
69
|
@writer = writer
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
# Directory entries are a no-op: 7z archives derive the
|
|
73
|
-
# directory tree from the files' archive paths ("sub/f.txt"
|
|
74
|
-
# implies "sub/"), and the writer/reader handle slashed
|
|
75
|
-
# paths (verified against 7zz).
|
|
76
72
|
def add(name, source_path = nil)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
if source_path
|
|
74
|
+
@writer.add_file(source_path, name)
|
|
75
|
+
else
|
|
76
|
+
@writer.add_directory_entry(name)
|
|
77
|
+
end
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
def add_data(name, data)
|
|
@@ -123,7 +123,11 @@ module Omnizip
|
|
|
123
123
|
@entry = entry
|
|
124
124
|
@reader = reader
|
|
125
125
|
@tmp = tmp
|
|
126
|
-
@name = entry.is_dir
|
|
126
|
+
@name = if entry.is_dir
|
|
127
|
+
entry.name.end_with?("/") ? entry.name : "#{entry.name}/"
|
|
128
|
+
else
|
|
129
|
+
entry.name
|
|
130
|
+
end
|
|
127
131
|
@size = entry.size
|
|
128
132
|
end
|
|
129
133
|
|
|
@@ -34,6 +34,15 @@ module Omnizip
|
|
|
34
34
|
# @param output_file [String] Path to output archive
|
|
35
35
|
# @param inputs [Array<String>] Paths to files/directories to archive
|
|
36
36
|
# @return [void]
|
|
37
|
+
# Execute the archive create command.
|
|
38
|
+
#
|
|
39
|
+
# Routes through the Archive facade: the handler registry
|
|
40
|
+
# picks each format's writer and applies its option semantics,
|
|
41
|
+
# so this command only translates CLI vocabulary and reports.
|
|
42
|
+
#
|
|
43
|
+
# @param output_file [String] Path to output archive
|
|
44
|
+
# @param inputs [Array<String>] Paths to files/directories to archive
|
|
45
|
+
# @return [void]
|
|
37
46
|
def run(output_file, *inputs)
|
|
38
47
|
validate_inputs(output_file, inputs)
|
|
39
48
|
|
|
@@ -44,220 +53,126 @@ module Omnizip
|
|
|
44
53
|
opts = apply_profile(first_file, opts)
|
|
45
54
|
end
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if format == "rar"
|
|
51
|
-
create_rar_archive(output_file, inputs, opts)
|
|
52
|
-
else
|
|
53
|
-
create_7z_archive(output_file, inputs, opts)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def detect_format(filename)
|
|
60
|
-
case File.extname(filename).downcase
|
|
61
|
-
when ".rar" then "rar"
|
|
62
|
-
else "7z"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def create_rar_archive(output_file, inputs, opts)
|
|
67
|
-
version = opts[:rar_version] || 5
|
|
68
|
-
compression = (opts[:rar_compression] || "store").to_sym
|
|
69
|
-
level = opts[:level] || 3
|
|
70
|
-
include_mtime = opts[:include_mtime] || false
|
|
71
|
-
include_crc32 = opts[:include_crc32] || false
|
|
72
|
-
solid = opts[:solid] || false
|
|
73
|
-
multi_volume = opts[:multi_volume] || false
|
|
74
|
-
volume_size = opts[:volume_size]
|
|
75
|
-
volume_naming = opts[:volume_naming] || "part"
|
|
76
|
-
password = opts[:password]
|
|
77
|
-
kdf_iterations = opts[:kdf_iterations] || 262_144
|
|
78
|
-
recovery = opts[:recovery] || false
|
|
79
|
-
recovery_percent = opts[:recovery_percent] || 5
|
|
56
|
+
format = (opts[:format] || detect_format(output_file)).to_sym
|
|
57
|
+
handler_opts = handler_options(format, opts)
|
|
80
58
|
verbose = opts[:verbose] || false
|
|
81
59
|
|
|
82
|
-
|
|
83
|
-
CliOutputFormatter.verbose_puts(
|
|
84
|
-
"Creating RAR#{version} archive: #{output_file}",
|
|
85
|
-
true,
|
|
86
|
-
)
|
|
87
|
-
CliOutputFormatter.verbose_puts(
|
|
88
|
-
"Compression: #{compression}, Level: #{level}",
|
|
89
|
-
true,
|
|
90
|
-
)
|
|
91
|
-
CliOutputFormatter.verbose_puts(
|
|
92
|
-
"Include mtime: #{include_mtime}, Include CRC32: #{include_crc32}",
|
|
93
|
-
true,
|
|
94
|
-
)
|
|
95
|
-
if solid
|
|
96
|
-
CliOutputFormatter.verbose_puts(
|
|
97
|
-
"Solid compression: enabled",
|
|
98
|
-
true,
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
if multi_volume && volume_size
|
|
102
|
-
CliOutputFormatter.verbose_puts(
|
|
103
|
-
"Multi-volume: enabled (size: #{volume_size}, naming: #{volume_naming})",
|
|
104
|
-
true,
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
if password
|
|
108
|
-
CliOutputFormatter.verbose_puts(
|
|
109
|
-
"Encryption: enabled (AES-256-CBC, KDF iterations: #{kdf_iterations})",
|
|
110
|
-
true,
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
if recovery
|
|
114
|
-
CliOutputFormatter.verbose_puts(
|
|
115
|
-
"PAR2 recovery: enabled (redundancy: #{recovery_percent}%)",
|
|
116
|
-
true,
|
|
117
|
-
)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
60
|
+
announce(output_file, format, handler_opts, opts, verbose)
|
|
120
61
|
|
|
121
62
|
start_time = Time.now
|
|
122
63
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
writer_opts = {
|
|
126
|
-
version: version,
|
|
127
|
-
compression: compression,
|
|
128
|
-
level: level,
|
|
129
|
-
include_mtime: include_mtime,
|
|
130
|
-
include_crc32: include_crc32,
|
|
131
|
-
solid: solid,
|
|
132
|
-
multi_volume: multi_volume,
|
|
133
|
-
volume_size: volume_size,
|
|
134
|
-
volume_naming: volume_naming,
|
|
135
|
-
password: password,
|
|
136
|
-
kdf_iterations: kdf_iterations,
|
|
137
|
-
recovery: recovery,
|
|
138
|
-
recovery_percent: recovery_percent,
|
|
139
|
-
}.compact
|
|
140
|
-
|
|
141
|
-
result_files = Omnizip::Formats::Rar.create(output_file,
|
|
142
|
-
writer_opts) do |rar|
|
|
64
|
+
Omnizip::Archive.create(output_file, format: format,
|
|
65
|
+
**handler_opts) do |archive|
|
|
143
66
|
inputs.each do |input|
|
|
144
67
|
if File.directory?(input)
|
|
145
68
|
CliOutputFormatter.verbose_puts(
|
|
146
69
|
"Adding directory: #{input}",
|
|
147
70
|
verbose,
|
|
148
71
|
)
|
|
149
|
-
|
|
150
|
-
# name, matching the .7z creation behavior
|
|
151
|
-
rar.add_directory(input, File.dirname(input))
|
|
72
|
+
archive.add_directory(input)
|
|
152
73
|
else
|
|
153
74
|
CliOutputFormatter.verbose_puts(
|
|
154
75
|
"Adding file: #{input}",
|
|
155
76
|
verbose,
|
|
156
77
|
)
|
|
157
|
-
|
|
78
|
+
archive.add_file(input)
|
|
158
79
|
end
|
|
159
80
|
end
|
|
160
81
|
end
|
|
161
82
|
|
|
162
83
|
elapsed = Time.now - start_time
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
files = result_files.is_a?(Array) ? result_files : [result_files]
|
|
166
|
-
files.find { |f| f.end_with?(".rar") } || files.first
|
|
167
|
-
archive_size = files.sum { |f| File.size(f) }
|
|
84
|
+
archive_size = calculate_archive_size(output_file,
|
|
85
|
+
handler_opts[:volume_size])
|
|
168
86
|
|
|
169
87
|
if verbose
|
|
170
88
|
puts ""
|
|
171
89
|
puts "Archive created successfully"
|
|
172
|
-
if
|
|
173
|
-
puts "Files created: #{files.size}"
|
|
174
|
-
puts " Archive volumes: #{files.count { |f| f.end_with?('.rar') }}"
|
|
175
|
-
if recovery
|
|
176
|
-
puts " PAR2 files: #{files.count { |f| f.include?('.par2') }}"
|
|
177
|
-
end
|
|
90
|
+
if handler_opts[:volume_size]
|
|
178
91
|
puts "Total size: #{format_bytes(archive_size)}"
|
|
92
|
+
puts "Volumes: #{count_volumes(output_file)}"
|
|
179
93
|
else
|
|
180
94
|
puts "Archive size: #{format_bytes(archive_size)}"
|
|
181
95
|
end
|
|
182
96
|
puts "Time elapsed: #{elapsed.round(2)}s"
|
|
183
|
-
elsif files.size == 1
|
|
184
|
-
puts "Created: #{files.first}"
|
|
185
97
|
else
|
|
186
|
-
puts "Created: #{
|
|
187
|
-
f.end_with?('.rar')
|
|
188
|
-
end} volumes)"
|
|
98
|
+
puts "Created: #{output_file}"
|
|
189
99
|
end
|
|
190
100
|
end
|
|
191
101
|
|
|
192
|
-
|
|
193
|
-
algorithm = (opts[:algorithm] || "lzma2").to_sym
|
|
194
|
-
level = opts[:level] || 5
|
|
195
|
-
solid = opts.fetch(:solid, true)
|
|
196
|
-
verbose = opts[:verbose] || false
|
|
197
|
-
filters = parse_filters(opts[:filters])
|
|
198
|
-
volume_size = parse_volume_size(opts[:volume_size])
|
|
199
|
-
password = opts[:password]
|
|
200
|
-
encrypt_headers = opts[:encrypt_headers] || false
|
|
201
|
-
preserve_ntfs_streams = opts[:preserve_ntfs_streams] || false
|
|
102
|
+
private
|
|
202
103
|
|
|
203
|
-
|
|
104
|
+
def detect_format(filename)
|
|
105
|
+
case File.extname(filename).downcase
|
|
106
|
+
when ".rar" then :rar
|
|
107
|
+
else :seven_zip
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Translate the format-agnostic CLI vocabulary into the option
|
|
112
|
+
# keywords each format's writer understands; nil values drop
|
|
113
|
+
def handler_options(format, opts)
|
|
114
|
+
case format
|
|
115
|
+
when :rar
|
|
116
|
+
rar_options(opts)
|
|
117
|
+
else
|
|
118
|
+
seven_zip_options(opts)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def rar_options(opts)
|
|
123
|
+
{
|
|
124
|
+
version: opts[:rar_version],
|
|
125
|
+
compression: opts[:rar_compression]&.to_sym,
|
|
126
|
+
level: opts[:level],
|
|
127
|
+
include_mtime: opts[:include_mtime],
|
|
128
|
+
include_crc32: opts[:include_crc32],
|
|
129
|
+
solid: opts[:solid],
|
|
130
|
+
multi_volume: opts[:multi_volume],
|
|
131
|
+
volume_size: opts[:volume_size],
|
|
132
|
+
volume_naming: opts[:volume_naming],
|
|
133
|
+
password: opts[:password],
|
|
134
|
+
kdf_iterations: opts[:kdf_iterations],
|
|
135
|
+
recovery: opts[:recovery],
|
|
136
|
+
recovery_percent: opts[:recovery_percent],
|
|
137
|
+
}.compact
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def seven_zip_options(opts)
|
|
141
|
+
{
|
|
142
|
+
algorithm: (opts[:algorithm] || "lzma2").to_sym,
|
|
143
|
+
level: opts[:level] || 5,
|
|
144
|
+
solid: opts.fetch(:solid, true),
|
|
145
|
+
filters: parse_filters(opts[:filters]),
|
|
146
|
+
volume_size: parse_volume_size(opts[:volume_size]),
|
|
147
|
+
password: opts[:password],
|
|
148
|
+
encrypt_headers: opts[:encrypt_headers] || false,
|
|
149
|
+
}.compact
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def announce(output_file, format, handler_opts, _opts, verbose)
|
|
153
|
+
return unless verbose
|
|
154
|
+
|
|
155
|
+
CliOutputFormatter.verbose_puts(
|
|
156
|
+
"Creating archive: #{output_file}",
|
|
157
|
+
true,
|
|
158
|
+
)
|
|
159
|
+
CliOutputFormatter.verbose_puts(
|
|
160
|
+
"Format: #{format == :rar ? "RAR#{handler_opts[:version] || 5}" : '7z'}, " \
|
|
161
|
+
"Algorithm: #{handler_opts[:algorithm] || handler_opts[:compression]}, " \
|
|
162
|
+
"Level: #{handler_opts[:level]}, Solid: #{handler_opts[:solid]}",
|
|
163
|
+
true,
|
|
164
|
+
)
|
|
165
|
+
if handler_opts[:volume_size]
|
|
204
166
|
CliOutputFormatter.verbose_puts(
|
|
205
|
-
"
|
|
167
|
+
"Volume size: #{format_bytes(handler_opts[:volume_size])}",
|
|
206
168
|
true,
|
|
207
169
|
)
|
|
170
|
+
end
|
|
171
|
+
if handler_opts[:password]
|
|
208
172
|
CliOutputFormatter.verbose_puts(
|
|
209
|
-
"
|
|
210
|
-
"Solid: #{solid}",
|
|
173
|
+
"Encryption: enabled",
|
|
211
174
|
true,
|
|
212
175
|
)
|
|
213
|
-
if volume_size
|
|
214
|
-
CliOutputFormatter.verbose_puts(
|
|
215
|
-
"Volume size: #{format_bytes(volume_size)}",
|
|
216
|
-
true,
|
|
217
|
-
)
|
|
218
|
-
end
|
|
219
|
-
unless filters.empty?
|
|
220
|
-
CliOutputFormatter.verbose_puts(
|
|
221
|
-
"Filters: #{filters.join(', ')}",
|
|
222
|
-
true,
|
|
223
|
-
)
|
|
224
|
-
end
|
|
225
|
-
if encrypt_headers
|
|
226
|
-
CliOutputFormatter.verbose_puts(
|
|
227
|
-
"Header encryption: enabled",
|
|
228
|
-
true,
|
|
229
|
-
)
|
|
230
|
-
end
|
|
231
|
-
if preserve_ntfs_streams && Omnizip::Platform.supports_ntfs_streams?
|
|
232
|
-
CliOutputFormatter.verbose_puts(
|
|
233
|
-
"NTFS streams: preserving",
|
|
234
|
-
true,
|
|
235
|
-
)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
start_time = Time.now
|
|
240
|
-
|
|
241
|
-
create_7z_archive_writer(output_file, inputs, algorithm, level, solid,
|
|
242
|
-
filters, volume_size, password, encrypt_headers,
|
|
243
|
-
preserve_ntfs_streams, verbose)
|
|
244
|
-
|
|
245
|
-
elapsed = Time.now - start_time
|
|
246
|
-
|
|
247
|
-
archive_size = calculate_archive_size(output_file, volume_size)
|
|
248
|
-
|
|
249
|
-
if verbose
|
|
250
|
-
puts ""
|
|
251
|
-
puts "Archive created successfully"
|
|
252
|
-
if volume_size
|
|
253
|
-
puts "Total size: #{format_bytes(archive_size)}"
|
|
254
|
-
puts "Volumes: #{count_volumes(output_file)}"
|
|
255
|
-
else
|
|
256
|
-
puts "Archive size: #{format_bytes(archive_size)}"
|
|
257
|
-
end
|
|
258
|
-
puts "Time elapsed: #{elapsed.round(2)}s"
|
|
259
|
-
else
|
|
260
|
-
puts "Created: #{output_file}"
|
|
261
176
|
end
|
|
262
177
|
end
|
|
263
178
|
|
|
@@ -289,44 +204,6 @@ module Omnizip
|
|
|
289
204
|
filter_str.split(",").map(&:strip).map(&:to_sym)
|
|
290
205
|
end
|
|
291
206
|
|
|
292
|
-
def create_7z_archive_writer(output_file, inputs, algorithm, level, solid,
|
|
293
|
-
filters, volume_size, password, encrypt_headers,
|
|
294
|
-
_preserve_ntfs_streams, verbose)
|
|
295
|
-
writer_opts = {
|
|
296
|
-
algorithm: algorithm,
|
|
297
|
-
level: level,
|
|
298
|
-
solid: solid,
|
|
299
|
-
filters: filters,
|
|
300
|
-
}
|
|
301
|
-
writer_opts[:volume_size] = volume_size if volume_size
|
|
302
|
-
writer_opts[:password] = password if password
|
|
303
|
-
writer_opts[:encrypt_headers] = encrypt_headers if encrypt_headers
|
|
304
|
-
|
|
305
|
-
writer = Formats::SevenZip::Writer.new(output_file, writer_opts)
|
|
306
|
-
|
|
307
|
-
inputs.each do |input|
|
|
308
|
-
if File.directory?(input)
|
|
309
|
-
CliOutputFormatter.verbose_puts(
|
|
310
|
-
"Adding directory: #{input}",
|
|
311
|
-
verbose,
|
|
312
|
-
)
|
|
313
|
-
writer.add_directory(input)
|
|
314
|
-
else
|
|
315
|
-
CliOutputFormatter.verbose_puts(
|
|
316
|
-
"Adding file: #{input}",
|
|
317
|
-
verbose,
|
|
318
|
-
)
|
|
319
|
-
writer.add_file(input)
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
CliOutputFormatter.verbose_puts("Writing archive...", verbose)
|
|
324
|
-
writer.write
|
|
325
|
-
rescue StandardError => e
|
|
326
|
-
raise Omnizip::CompressionError,
|
|
327
|
-
"Failed to create archive: #{e.message}"
|
|
328
|
-
end
|
|
329
|
-
|
|
330
207
|
def format_bytes(bytes)
|
|
331
208
|
units = %w[B KB MB GB]
|
|
332
209
|
size = bytes.to_f
|
|
@@ -184,7 +184,9 @@ module Omnizip
|
|
|
184
184
|
warn "RAR4 writer ignores volume_size: not implemented " \
|
|
185
185
|
"(no corresponding flag is written)"
|
|
186
186
|
end
|
|
187
|
-
|
|
187
|
+
recovery = @options[:recovery]
|
|
188
|
+
return unless recovery.is_a?(Numeric) && recovery.positive?
|
|
189
|
+
return if recovery == true
|
|
188
190
|
|
|
189
191
|
warn "RAR4 writer ignores recovery: not implemented " \
|
|
190
192
|
"(no corresponding flag is written)"
|
|
@@ -75,7 +75,9 @@ module Omnizip
|
|
|
75
75
|
# @return [self]
|
|
76
76
|
def add_directory_entry(archive_path)
|
|
77
77
|
entry = Models::FileEntry.new
|
|
78
|
-
|
|
78
|
+
# Trailing slash is preserved: explicit directory entries
|
|
79
|
+
# carry it ("testdir/"), matching the collector's tree walk
|
|
80
|
+
entry.name = archive_path
|
|
79
81
|
entry.source_path = nil
|
|
80
82
|
entry.size = 0
|
|
81
83
|
entry.is_dir = true
|
data/lib/omnizip/version.rb
CHANGED