omnizip 0.3.50 → 0.3.51
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 +13 -0
- data/lib/omnizip/commands/archive_create_command.rb +1 -38
- data/lib/omnizip/convenience.rb +58 -73
- data/lib/omnizip/converter/conversion_strategy.rb +19 -30
- data/lib/omnizip/converter/extract_repack_strategy.rb +2 -12
- data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +3 -14
- data/lib/omnizip/converter/zip_to_seven_zip_strategy.rb +4 -15
- data/lib/omnizip/profile.rb +72 -0
- data/lib/omnizip/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b268c5b315ddcfff3347f6843e30e7ca180b1b3bae14b21a7a721bd2ccf3f6e
|
|
4
|
+
data.tar.gz: 2d150420264453af6e1ba5114b32a31b1a67c12a020f41435a037747dec2bfde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '053812e04bf42b6fe5335d57a5183c90b19a3ed8b414ee57ac9a83831a04f15e7225ecc79d29bc42e78e692bf7fe6f60b88ba234a3fc92eb0e781e7a357acd14'
|
|
7
|
+
data.tar.gz: dd2e32bd6ea8732be35a2f6c7325ed23b922860c051caf7666df93edd7f7da1342bdc1f6c9928346a404a7a1c707c509bd74d1b281ddd07cdc4a591c67e8a05d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.50] - 2026-08-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- The conversion matrix is closed: `Omnizip::Converter.convert` now
|
|
14
|
+
converts any routed source format (ZIP, 7z, TAR, RAR, cpio, ISO,
|
|
15
|
+
RPM, XAR, MSI) into any writable target format (ZIP, 7z, TAR,
|
|
16
|
+
RAR) through a new extract-and-repack strategy built on the
|
|
17
|
+
handler registry. The documented RAR→ZIP and TAR→7z pairs
|
|
18
|
+
previously had no strategy behind them — `Converter.supported?`
|
|
19
|
+
answered false for conversions the README promised. The direct
|
|
20
|
+
ZIP↔7z strategies keep precedence, so entry-copying conversions
|
|
21
|
+
don't take the repack detour.
|
|
22
|
+
|
|
10
23
|
## [0.3.49] - 2026-08-31
|
|
11
24
|
|
|
12
25
|
### Added
|
|
@@ -47,11 +47,7 @@ module Omnizip
|
|
|
47
47
|
validate_inputs(output_file, inputs)
|
|
48
48
|
|
|
49
49
|
# Apply profile settings if specified
|
|
50
|
-
opts = options
|
|
51
|
-
if opts[:profile]
|
|
52
|
-
first_file = find_first_file(inputs)
|
|
53
|
-
opts = apply_profile(first_file, opts)
|
|
54
|
-
end
|
|
50
|
+
opts = Omnizip::Profile.apply(options, paths: inputs)
|
|
55
51
|
|
|
56
52
|
format = (opts[:format] || detect_format(output_file)).to_sym
|
|
57
53
|
handler_opts = handler_options(format, opts)
|
|
@@ -217,39 +213,6 @@ module Omnizip
|
|
|
217
213
|
format("%.2f %s", size, units[unit_idx])
|
|
218
214
|
end
|
|
219
215
|
|
|
220
|
-
def apply_profile(file_path, options)
|
|
221
|
-
profile_spec = options.delete(:profile)
|
|
222
|
-
return options unless profile_spec
|
|
223
|
-
|
|
224
|
-
# Get the profile
|
|
225
|
-
profile = case profile_spec
|
|
226
|
-
when "auto"
|
|
227
|
-
file_path ? Omnizip::Profile.detect(file_path) : Omnizip::Profile.get(:balanced)
|
|
228
|
-
else
|
|
229
|
-
Omnizip::Profile.get(profile_spec.to_sym) || Omnizip::Profile.get(:balanced)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Apply profile to options
|
|
233
|
-
profile.apply_to(options)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def find_first_file(inputs)
|
|
237
|
-
inputs.each do |input|
|
|
238
|
-
return input if File.file?(input)
|
|
239
|
-
|
|
240
|
-
# Check directories for first file
|
|
241
|
-
if File.directory?(input)
|
|
242
|
-
Dir.foreach(input) do |entry|
|
|
243
|
-
next if [".", ".."].include?(entry)
|
|
244
|
-
|
|
245
|
-
full_path = File.join(input, entry)
|
|
246
|
-
return full_path if File.file?(full_path)
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
nil
|
|
251
|
-
end
|
|
252
|
-
|
|
253
216
|
def parse_volume_size(size_str)
|
|
254
217
|
return nil if size_str.nil? || size_str.empty?
|
|
255
218
|
|
data/lib/omnizip/convenience.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Omnizip
|
|
|
25
25
|
raise ArgumentError, "Input is a directory: #{input_path}"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
options =
|
|
28
|
+
options = Omnizip::Profile.apply(options, paths: input_path)
|
|
29
29
|
|
|
30
30
|
if options[:chunked]
|
|
31
31
|
return Omnizip::Chunked.compress_file(input_path, output_path, **options)
|
|
@@ -57,30 +57,14 @@ module Omnizip
|
|
|
57
57
|
def decompress_file(compressed_path, output_path)
|
|
58
58
|
require_archive!(compressed_path)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
unless
|
|
60
|
+
decompressor = SINGLE_FILE_DECOMPRESSORS[::File.extname(compressed_path).downcase]
|
|
61
|
+
unless decompressor
|
|
62
62
|
raise Omnizip::UnsupportedFormatError,
|
|
63
|
-
"decompress_file supports #{
|
|
63
|
+
"decompress_file supports #{SINGLE_FILE_DECOMPRESSORS.keys.join(', ')}; " \
|
|
64
64
|
"use extract_archive for archive formats"
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
when :xz
|
|
69
|
-
::File.binwrite(output_path, Formats::Xz.decompress(compressed_path))
|
|
70
|
-
when :zstandard
|
|
71
|
-
zstd = Algorithms::Zstandard.new
|
|
72
|
-
::File.open(compressed_path, "rb") do |input_io|
|
|
73
|
-
::File.open(output_path, "wb") do |output_io|
|
|
74
|
-
zstd.decompress(input_io, output_io)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
else
|
|
78
|
-
::File.open(compressed_path, "rb") do |input_io|
|
|
79
|
-
::File.open(output_path, "wb") do |output_io|
|
|
80
|
-
format_class.decompress_stream(input_io, output_io)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
67
|
+
decompressor.call(compressed_path, output_path)
|
|
84
68
|
output_path
|
|
85
69
|
end
|
|
86
70
|
|
|
@@ -112,10 +96,7 @@ module Omnizip
|
|
|
112
96
|
"individually"
|
|
113
97
|
end
|
|
114
98
|
|
|
115
|
-
|
|
116
|
-
first_file = find_first_file(input_dir)
|
|
117
|
-
apply_profile(first_file, options)
|
|
118
|
-
end
|
|
99
|
+
options = Omnizip::Profile.apply(options, paths: input_dir)
|
|
119
100
|
|
|
120
101
|
Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
|
|
121
102
|
add_directory_contents(archive, input_dir, "", recursive: recursive)
|
|
@@ -212,11 +193,17 @@ module Omnizip
|
|
|
212
193
|
archive_path
|
|
213
194
|
end
|
|
214
195
|
|
|
215
|
-
# Create a RAR archive
|
|
196
|
+
# Create a RAR archive through the RAR handler. RAR5 unless
|
|
197
|
+
# +version: 4+; the block yields the same generic writer
|
|
198
|
+
# interface as +Archive.create+ (+add+/+add_data+/+add_directory+).
|
|
199
|
+
#
|
|
200
|
+
# @param archive_path [String] Path to output archive
|
|
201
|
+
# @param options [Hash] RAR writer options
|
|
202
|
+
# @return [String, Array<String>] Path(s) of the created archive
|
|
216
203
|
# rubocop:disable-next Naming/BlockForwarding, Style/ArgumentsForwarding -- Ruby 3.0 compatibility
|
|
217
204
|
def create_rar(archive_path, **options, &block)
|
|
218
205
|
options[:version] ||= 5
|
|
219
|
-
Omnizip::
|
|
206
|
+
Omnizip::ArchiveHandler.for(:rar).create(archive_path, **options, &block)
|
|
220
207
|
end
|
|
221
208
|
|
|
222
209
|
# Extension -> single-file compressor map. Each lambda takes
|
|
@@ -249,18 +236,50 @@ module Omnizip
|
|
|
249
236
|
".msi" => :ole,
|
|
250
237
|
}.freeze
|
|
251
238
|
|
|
252
|
-
# Extension -> single-file decompressor
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
".gz" =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
239
|
+
# Extension -> single-file decompressor. Each lambda takes
|
|
240
|
+
# (compressed_path, output_path) and writes the decompressed
|
|
241
|
+
# bytes; these formats are streams, not archives, so they bypass
|
|
242
|
+
# ArchiveHandler (mirrors SINGLE_FILE_COMPRESSORS). Xz exposes a
|
|
243
|
+
# path-based decompress rather than a stream one.
|
|
244
|
+
SINGLE_FILE_DECOMPRESSORS = {
|
|
245
|
+
".gz" => lambda do |compressed, output|
|
|
246
|
+
::File.open(compressed, "rb") do |input_io|
|
|
247
|
+
::File.open(output, "wb") do |output_io|
|
|
248
|
+
Omnizip::Formats::Gzip.decompress_stream(input_io, output_io)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end,
|
|
252
|
+
".bz2" => lambda do |compressed, output|
|
|
253
|
+
::File.open(compressed, "rb") do |input_io|
|
|
254
|
+
::File.open(output, "wb") do |output_io|
|
|
255
|
+
Omnizip::Formats::Bzip2File.decompress_stream(input_io, output_io)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end,
|
|
259
|
+
".xz" => lambda do |compressed, output|
|
|
260
|
+
::File.binwrite(output, Omnizip::Formats::Xz.decompress(compressed))
|
|
261
|
+
end,
|
|
262
|
+
".lzma" => lambda do |compressed, output|
|
|
263
|
+
::File.open(compressed, "rb") do |input_io|
|
|
264
|
+
::File.open(output, "wb") do |output_io|
|
|
265
|
+
Omnizip::Formats::LzmaAlone.decompress_stream(input_io, output_io)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end,
|
|
269
|
+
".lz" => lambda do |compressed, output|
|
|
270
|
+
::File.open(compressed, "rb") do |input_io|
|
|
271
|
+
::File.open(output, "wb") do |output_io|
|
|
272
|
+
Omnizip::Formats::Lzip.decompress_stream(input_io, output_io)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end,
|
|
276
|
+
".zst" => lambda do |compressed, output|
|
|
277
|
+
::File.open(compressed, "rb") do |input_io|
|
|
278
|
+
::File.open(output, "wb") do |output_io|
|
|
279
|
+
Omnizip::Algorithms::Zstandard.new.decompress(input_io, output_io)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end,
|
|
264
283
|
}.freeze
|
|
265
284
|
|
|
266
285
|
SINGLE_FILE_COMPRESSORS = {
|
|
@@ -335,40 +354,6 @@ module Omnizip
|
|
|
335
354
|
raise Errno::ENOENT, "Archive not found: #{archive_path}"
|
|
336
355
|
end
|
|
337
356
|
|
|
338
|
-
# Apply compression profile to options.
|
|
339
|
-
def apply_profile(file_path, options)
|
|
340
|
-
profile_spec = options.delete(:profile)
|
|
341
|
-
return options unless profile_spec
|
|
342
|
-
|
|
343
|
-
profile = case profile_spec
|
|
344
|
-
when :auto
|
|
345
|
-
file_path ? Omnizip::Profile.detect(file_path) : Omnizip::Profile.get(:balanced)
|
|
346
|
-
when Symbol
|
|
347
|
-
Omnizip::Profile.get(profile_spec) || Omnizip::Profile.get(:balanced)
|
|
348
|
-
when Omnizip::Profile::CompressionProfile
|
|
349
|
-
profile_spec
|
|
350
|
-
else
|
|
351
|
-
Omnizip::Profile.get(:balanced)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
profile.apply_to(options)
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def find_first_file(dir_path)
|
|
358
|
-
Dir.foreach(dir_path) do |entry|
|
|
359
|
-
next if [".", ".."].include?(entry)
|
|
360
|
-
|
|
361
|
-
full_path = ::File.join(dir_path, entry)
|
|
362
|
-
return full_path if ::File.file?(full_path)
|
|
363
|
-
|
|
364
|
-
if ::File.directory?(full_path)
|
|
365
|
-
result = find_first_file(full_path)
|
|
366
|
-
return result if result
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
nil
|
|
370
|
-
end
|
|
371
|
-
|
|
372
357
|
# Recursively add directory contents to an archive. The +archive+
|
|
373
358
|
# argument is whatever the handler's +#create+ block yields (e.g.
|
|
374
359
|
# +Omnizip::Zip::File+ or +Omnizip::Formats::Tar::Writer+) and must
|
|
@@ -60,19 +60,26 @@ module Omnizip
|
|
|
60
60
|
@warnings
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
# Repack the extracted tree under +extracted_dir+ into a new
|
|
64
|
+
# archive at +target_path+ through the Archive facade (the
|
|
65
|
+
# format is resolved from the target's extension). Directory
|
|
66
|
+
# entries are implied by file paths, matching every strategy's
|
|
67
|
+
# treatment. Returns the number of file entries written.
|
|
68
|
+
#
|
|
69
|
+
# @param extracted_dir [String] Directory of extracted entries
|
|
70
|
+
# @param write_options [Hash] Format writer options
|
|
71
|
+
# @return [Integer] Number of file entries written
|
|
72
|
+
def repack_tree(extracted_dir, **write_options)
|
|
73
|
+
count = 0
|
|
74
|
+
Omnizip::Archive.create(target_path, **write_options) do |archive|
|
|
75
|
+
Dir.glob(File.join(extracted_dir, "**", "*")).each do |path|
|
|
76
|
+
next if File.directory?(path)
|
|
77
|
+
|
|
78
|
+
archive.add_file(path, path.delete_prefix("#{extracted_dir}/"))
|
|
79
|
+
count += 1
|
|
80
|
+
end
|
|
75
81
|
end
|
|
82
|
+
count
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
# Create conversion result
|
|
@@ -96,24 +103,6 @@ module Omnizip
|
|
|
96
103
|
warnings: warnings,
|
|
97
104
|
)
|
|
98
105
|
end
|
|
99
|
-
|
|
100
|
-
# Check if metadata is compatible between formats
|
|
101
|
-
# @param entry [Entry] Entry to check
|
|
102
|
-
# @return [Boolean] True if fully compatible
|
|
103
|
-
def metadata_compatible?(_entry)
|
|
104
|
-
# ZIP supports most metadata
|
|
105
|
-
# 7z has limited metadata support
|
|
106
|
-
case [source_format, target_format]
|
|
107
|
-
when %i[zip seven_zip]
|
|
108
|
-
# Some metadata loss (comments, extra fields)
|
|
109
|
-
false
|
|
110
|
-
when %i[seven_zip zip]
|
|
111
|
-
# Can preserve most 7z metadata in ZIP
|
|
112
|
-
true
|
|
113
|
-
else
|
|
114
|
-
true
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
106
|
end
|
|
118
107
|
end
|
|
119
108
|
end
|
|
@@ -34,18 +34,8 @@ module Omnizip
|
|
|
34
34
|
|
|
35
35
|
Dir.mktmpdir("omnizip_convert_repack") do |tmp|
|
|
36
36
|
extracted = Omnizip.extract_archive(source_path, tmp)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
format: self.class.target_format_for(target_path)) do |b|
|
|
40
|
-
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
41
|
-
next if File.directory?(path)
|
|
42
|
-
|
|
43
|
-
b.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
entry_count = extracted.size
|
|
48
|
-
create_result(start_time, entry_count)
|
|
37
|
+
repack_tree(tmp)
|
|
38
|
+
create_result(start_time, extracted.size)
|
|
49
39
|
end
|
|
50
40
|
end
|
|
51
41
|
|
|
@@ -11,20 +11,9 @@ module Omnizip
|
|
|
11
11
|
def convert
|
|
12
12
|
start_time = Time.now
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Dir.mktmpdir("omnizip_convert") do |tmp|
|
|
19
|
-
reader.extract_all(tmp)
|
|
20
|
-
|
|
21
|
-
writer = Omnizip::Formats::Zip::Writer.new(target_path)
|
|
22
|
-
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
23
|
-
next if File.directory?(path)
|
|
24
|
-
|
|
25
|
-
writer.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
26
|
-
end
|
|
27
|
-
writer.write
|
|
14
|
+
entry_count = Dir.mktmpdir("omnizip_convert") do |tmp|
|
|
15
|
+
Omnizip.extract_archive(source_path, tmp)
|
|
16
|
+
repack_tree(tmp)
|
|
28
17
|
end
|
|
29
18
|
|
|
30
19
|
create_result(start_time, entry_count)
|
|
@@ -10,22 +10,11 @@ module Omnizip
|
|
|
10
10
|
# @return [ConversionResult] Conversion result
|
|
11
11
|
def convert
|
|
12
12
|
start_time = Time.now
|
|
13
|
-
entry_count = 0
|
|
14
13
|
|
|
15
|
-
Dir.mktmpdir("omnizip_convert_zip") do |tmp|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
writer = Omnizip::Formats::SevenZip::Writer.new(target_path,
|
|
21
|
-
writer_options)
|
|
22
|
-
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
23
|
-
next if File.directory?(path)
|
|
24
|
-
|
|
25
|
-
entry_count += 1
|
|
26
|
-
writer.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
27
|
-
end
|
|
28
|
-
writer.write
|
|
14
|
+
entry_count = Dir.mktmpdir("omnizip_convert_zip") do |tmp|
|
|
15
|
+
# Directories are implied by extracted file paths
|
|
16
|
+
Omnizip.extract_archive(source_path, tmp)
|
|
17
|
+
repack_tree(tmp, **writer_options)
|
|
29
18
|
end
|
|
30
19
|
|
|
31
20
|
create_result(start_time, entry_count)
|
data/lib/omnizip/profile.rb
CHANGED
|
@@ -108,6 +108,62 @@ module Omnizip
|
|
|
108
108
|
detector.detect(file_path, options)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Resolve a profile specification into a concrete profile.
|
|
112
|
+
# +:auto+ (or "auto") samples +first_file+ through detection and
|
|
113
|
+
# falls back to +:balanced+ without one; a CompressionProfile
|
|
114
|
+
# passes through unchanged; anything else names a registered
|
|
115
|
+
# profile, falling back to +:balanced+ when unknown.
|
|
116
|
+
#
|
|
117
|
+
# @param spec [Symbol, String, CompressionProfile] Profile spec
|
|
118
|
+
# @param first_file [String, nil] Sampling file for :auto
|
|
119
|
+
# @return [CompressionProfile] Resolved profile
|
|
120
|
+
def resolve(spec, first_file: nil)
|
|
121
|
+
case spec
|
|
122
|
+
when :auto, "auto"
|
|
123
|
+
first_file ? detect(first_file) : registry.get(:balanced)
|
|
124
|
+
when CompressionProfile
|
|
125
|
+
spec
|
|
126
|
+
else
|
|
127
|
+
registry.get(spec.to_sym) || registry.get(:balanced)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Resolve and apply the +:profile+ entry of +options+ into
|
|
132
|
+
# concrete compression settings. +:auto+ profiles sample the
|
|
133
|
+
# first readable file at or below +paths+. Returns a new
|
|
134
|
+
# options hash; +options+ itself is left untouched.
|
|
135
|
+
#
|
|
136
|
+
# @param options [Hash] Compression options carrying :profile
|
|
137
|
+
# @param paths [String, Array<String>] Inputs to sample
|
|
138
|
+
# @return [Hash] Options with the profile applied and removed
|
|
139
|
+
def apply(options, paths: [])
|
|
140
|
+
options = options.dup
|
|
141
|
+
spec = options.delete(:profile)
|
|
142
|
+
return options unless spec
|
|
143
|
+
|
|
144
|
+
resolve(spec, first_file: first_file_among(paths)).apply_to(options)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The first regular file at or below any of +paths+ — the
|
|
148
|
+
# sampling target for +:auto+ profiles — or nil when nothing
|
|
149
|
+
# readable exists.
|
|
150
|
+
#
|
|
151
|
+
# @param paths [String, Array<String>] Files or directories
|
|
152
|
+
# @return [String, nil] Path of the first file found
|
|
153
|
+
def first_file_among(paths)
|
|
154
|
+
Array(paths).each do |path|
|
|
155
|
+
next unless ::File.exist?(path)
|
|
156
|
+
|
|
157
|
+
return path if ::File.file?(path)
|
|
158
|
+
|
|
159
|
+
if ::File.directory?(path)
|
|
160
|
+
found = first_file_within(path)
|
|
161
|
+
return found if found
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
111
167
|
# Get the profile detector
|
|
112
168
|
#
|
|
113
169
|
# @return [ProfileDetector] The detector instance
|
|
@@ -125,6 +181,22 @@ module Omnizip
|
|
|
125
181
|
|
|
126
182
|
private
|
|
127
183
|
|
|
184
|
+
# Depth-first search for the first regular file under +dir+.
|
|
185
|
+
def first_file_within(dir)
|
|
186
|
+
::Dir.foreach(dir) do |entry|
|
|
187
|
+
next if [".", ".."].include?(entry)
|
|
188
|
+
|
|
189
|
+
full_path = ::File.join(dir, entry)
|
|
190
|
+
return full_path if ::File.file?(full_path)
|
|
191
|
+
|
|
192
|
+
if ::File.directory?(full_path)
|
|
193
|
+
found = first_file_within(full_path)
|
|
194
|
+
return found if found
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
128
200
|
# Register all built-in profiles
|
|
129
201
|
#
|
|
130
202
|
# @param registry [ProfileRegistry] Registry to populate
|
data/lib/omnizip/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.51
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|