rom-distillery 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,433 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ require 'set'
4
+ require 'pathname'
5
+ require 'fileutils'
6
+ require 'find'
7
+
8
+ require_relative 'rom'
9
+ require_relative 'rom-archive'
10
+
11
+ module Distillery
12
+
13
+ class Vault
14
+ include Enumerable
15
+
16
+ # @!visibility private
17
+ GLOB_PATTERN_REGEX = /(?<!\\)[?*}{\[\]]/
18
+
19
+ # List of ROM checksums
20
+ CHECKSUMS = ROM::CHECKSUMS
21
+
22
+ # List of archives extensions
23
+ ARCHIVES = ROMArchive::EXTENSIONS
24
+
25
+ # List of files to be ignored
26
+ IGNORE_FILES = Set[ '.dat', '.missing', '.baddump', '.extra' ]
27
+
28
+ # List of directories to be ignored
29
+ IGNORE_DIRS = Set[ '.roms', '.games', '.trash' ]
30
+
31
+ # Directory pruning
32
+ DIR_PRUNING = Set[ '.dat' ]
33
+
34
+
35
+ # Potential ROM from directory.
36
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
37
+ # directories holding a {DIR_PRUNING} file or starting with a
38
+ # dot are ignored
39
+ #
40
+ # @param dir [String] path to directory
41
+ # @param depth [Integer,nil] exploration depth
42
+ #
43
+ # @yieldparam file [String] file being processed
44
+ # @yieldparam dir: [String] directory relative to
45
+ #
46
+ def self.from_dir(dir, depth: nil)
47
+ Find.find(dir) do |path|
48
+ basename = File.basename(path)
49
+ subpath = Pathname(path).relative_path_from(dir).to_s
50
+ if FileTest.directory?(path)
51
+ next if path == dir
52
+ Find.prune if IGNORE_DIRS.include?(basename)
53
+ Find.prune if basename.start_with?('.')
54
+ Find.prune if !depth.nil? &&
55
+ subpath.split(File::Separator).size > depth
56
+ Find.prune if DIR_PRUNING.any? {|f| File.exists?(f) }
57
+ elsif FileTest.file?(path)
58
+ next if IGNORE_FILES.include?(basename)
59
+ yield(subpath, dir: dir) if block_given?
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+ # Potential ROM from glob
66
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
67
+ # directories holding a {DIR_PRUNING} file or starting with a
68
+ # dot are ignored
69
+ #
70
+ # @param glob [String] ruby glob
71
+ # @param basedir [:guess,nil] basedir to use when interpreting glob
72
+ # matching
73
+ #
74
+ # @yieldparam file [String] file being processed
75
+ # @yieldparam dir: [String] directory relative to
76
+ #
77
+ def self.from_glob(glob, basedir: :guess)
78
+ if basedir == :guess
79
+ gentry = glob.split(File::SEPARATOR)
80
+ idx = gentry.find_index{|entry| entry =~ GLOB_PATTERN_REGEX }
81
+ gentry = gentry[0,idx]
82
+ basedir = if gentry.empty? then nil
83
+ elsif gentry.first.empty? then '/'
84
+ else File.join(gentry)
85
+ end
86
+ end
87
+
88
+ # Build file list (reject ignored files and dirs)
89
+ lst = Dir[glob].reject {|path|
90
+ (! FileTest.file?(path)) ||
91
+ IGNORE_FILES.include?(File.basename(path)) ||
92
+ path.split(File::SEPARATOR)[0..-1].any? {|dir|
93
+ IGNORE_DIRS.include?(dir) || dir.start_with?('.')
94
+ }
95
+ }
96
+ # Build cut list based on directory prunning
97
+ cutlst = lst.map {|f| File.dirname(f) }.uniq.select {|f|
98
+ DIR_PRUNING.any? {|p| FileTest.exist?(File.join(f,p)) }
99
+ }
100
+ # Apply cut list
101
+ lst.reject! {|path|
102
+ cutlst.any? {|cut| path.start_with?("#{cut}#{File::SEPARATOR}") }
103
+ }
104
+
105
+ # Iterate on list
106
+ lst.each do |path|
107
+ subpath = if basedir.nil?
108
+ then path
109
+ else Pathname(path).relative_path_from(basedir).to_s
110
+ end
111
+ yield(subpath, dir: basedir) if block_given?
112
+ end
113
+ end
114
+
115
+
116
+ def initialize(roms = [])
117
+ @cksum = Hash[CHECKSUMS.map {|k| [ k, {} ] }]
118
+ @roms = []
119
+
120
+ Array(roms).each {|rom| add_rom(rom) }
121
+ end
122
+
123
+ # @return [Boolean]
124
+ def empty?
125
+ @roms.empty?
126
+ end
127
+
128
+ # @return [Integer]
129
+ def size
130
+ @roms.size
131
+ end
132
+
133
+ # Iterate over each ROM
134
+ #
135
+ # @yieldparam rom [ROM]
136
+ #
137
+ # @return [self,Enumerator]
138
+ #
139
+ def each
140
+ block_given? ? @roms.each {|r| yield(r) }
141
+ : @roms.each
142
+ end
143
+
144
+ # Construct a new ROM vault as the intersection
145
+ #
146
+ # @param o [Vault] ROM vault to intersect with self
147
+ #
148
+ # @return [Vault]
149
+ def &(o)
150
+ Vault::new(@roms.select {|rom| o.match(rom) })
151
+ end
152
+
153
+
154
+ # Constuct a new ROM vault as the difference
155
+ #
156
+ # @param o [Vault] ROM vault to substract to self
157
+ #
158
+ # @return [Vault]
159
+ def -(o)
160
+ Vault::new(@roms.reject {|rom| o.match(rom) })
161
+ end
162
+
163
+
164
+ # Add ROM
165
+ #
166
+ # @param [ROM] *roms ROM to add
167
+ #
168
+ # @return self
169
+ #
170
+ def <<(rom)
171
+ add_rom(rom)
172
+ end
173
+
174
+
175
+ # Add ROM
176
+ #
177
+ # @param [ROM] rom ROM to add
178
+ #
179
+ # @return self
180
+ #
181
+ def add_rom(rom)
182
+ # Sanity check
183
+ unless ROM === rom
184
+ raise ArgumentError, "not a ROM"
185
+ end
186
+
187
+ # Add it to the list
188
+ @roms << rom
189
+
190
+ # Keep track of checksums
191
+ @cksum.each {|type, hlist|
192
+ hlist.merge!(rom.cksum(type) => rom) {|key, old, new|
193
+ if Array(old).any? {|r| r.path == new.path}
194
+ then old
195
+ else Array(old) + [ new ]
196
+ end
197
+ }
198
+ }
199
+
200
+ # Chainable
201
+ self
202
+ end
203
+
204
+
205
+ # Add ROM from file
206
+ #
207
+ # @param file [String] path to files relative to basedir
208
+ # @param basedir [String,nil] base directory
209
+ # @param archives [#include?] archives tester
210
+ #
211
+ # @return [self]
212
+ #
213
+ def add_from_file(file, basedir = nil, archives: ARCHIVES)
214
+ filepath = File.join(*[ basedir, file ].compact)
215
+ romlist = if ROMArchive.archive?(filepath, archives: archives)
216
+ then ROMArchive.from_file(filepath).to_a
217
+ else ROM.from_file(file, basedir)
218
+ end
219
+
220
+ Array(romlist).each {|rom| add_rom(rom) }
221
+ end
222
+
223
+
224
+ # Add ROM from directory.
225
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
226
+ # directories holding a {DIR_PRUNING} file or starting with a
227
+ # dot are ignored
228
+ #
229
+ # @param dir [String] path to directory
230
+ # @param depth [Integer,nil] exploration depth
231
+ # @param archives [#include?] archives tester
232
+ #
233
+ # @yieldparam file [String] file being processed
234
+ # @yieldparam dir [String] directory relative to
235
+ #
236
+ # @return [self]
237
+ #
238
+ def add_from_dir(dir, depth: nil, archives: ARCHIVES)
239
+ Vault.from_dir(dir, depth: depth) do | file, dir: |
240
+ yield(file, dir: dir) if block_given?
241
+ add_from_file(file, dir, archives: archives)
242
+ end
243
+ self
244
+ end
245
+
246
+
247
+ # Add ROM from glob
248
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
249
+ # directories holding a {DIR_PRUNING} file or starting with a
250
+ # dot are ignored
251
+ #
252
+ # @param glob [String] ruby glob
253
+ # @param basedir [:guess,nil] basedir to use when interpreting glob
254
+ # matching
255
+ # @param archives [#include?] archives tester
256
+ #
257
+ # @yieldparam file [String] file being processed
258
+ # @yieldparam dir [String] directory relative to
259
+ #
260
+ # @return [self]
261
+ #
262
+ def add_from_glob(glob, basedir: :guess, archives: ARCHIVES)
263
+ Vault.from_dir(glob, basedir: basedir) do | file, dir: |
264
+ yield(file, dir: dir) if block_given?
265
+ add_from_file(file, dir, archives: archives)
266
+ end
267
+ self
268
+ end
269
+
270
+
271
+ # List of ROM with loosely defined (ie: with some missing checksum)
272
+ #
273
+ # @return [Array<ROM>,nil]
274
+ #
275
+ def with_partial_checksum
276
+ @roms.select {|rom| rom.missing_checksums? }
277
+ end
278
+
279
+
280
+ # Check if we have some headered ROM.
281
+ #
282
+ # @return [Integer] only some ROMs are headered
283
+ # @return [true] all ROMs are headered
284
+ # @return [false] no headered ROM
285
+ #
286
+ def headered
287
+ size = @roms.select {|rom| rom.headered? }.size
288
+
289
+ if size == 0 then false
290
+ elsif size == @roms.size then true
291
+ else size
292
+ end
293
+ end
294
+
295
+ # Return list of matching ROMs.
296
+ #
297
+ # @param query [Hash{Symbol=>String}] Hash of checksums to match with
298
+ #
299
+ # @return [Array<ROM>] list of matching ROMs
300
+ # @return [nil] if no match
301
+ #
302
+ def cksummatch(query)
303
+ CHECKSUMS.each {|type|
304
+ if (q = query[type]) && (r = @cksum[type][q])
305
+ return Array(r)
306
+ end
307
+ }
308
+ return nil
309
+ end
310
+
311
+ # Return list of matching ROMs.
312
+ #
313
+ # @param rom [ROM] ROM to match with
314
+ #
315
+ # @return [Array<ROM>] list of matching ROMs
316
+ # @return [nil] if no match
317
+ #
318
+ def rommatch(rom)
319
+ self.cksummatch(rom.cksums)
320
+ end
321
+
322
+
323
+ # Return list of matching ROMs.
324
+ #
325
+ # @param query [Hash{Symbol=>String},ROM] Hash of checksums or ROM
326
+ # to match with
327
+ #
328
+ # @yieldparam rom [ROM] ROM that has been saved
329
+ #
330
+ # @return [Array<ROM>] list of matching ROMs
331
+ # @return [nil] if no match
332
+ #
333
+ def match(query)
334
+ case query
335
+ when Hash then self.cksummatch(query)
336
+ when ROM then self.rommatch(query)
337
+ else raise ArgumentError
338
+ end
339
+ end
340
+
341
+
342
+ # Save ROM to filesystem
343
+ #
344
+ # @param dir [String] directory used for saving
345
+ # @param part [:all,:header,:rom] wich part of the ROM file to save
346
+ # @param subdir [Boolean,Integer,Proc] use subdirectory
347
+ # @param pristine [Boolean] should existing directory be removed
348
+ # @param force [Boolean] remove previous file if necessary
349
+ #
350
+ # @yieldparam rom [ROM] ROM saved
351
+ #
352
+ # @return [self]
353
+ #
354
+ def save(dir, part: :all, subdir: false, pristine: false, force: false,
355
+ &block)
356
+ # Directory
357
+ FileUtils.remove_dir(dir) if pristine # Create clean env
358
+ Dir.mkdir(dir) unless Dir.exist?(dir) # Ensure directory
359
+
360
+ # Fill directory.
361
+ # -> We have the physical ROMs, so we have all the checksums
362
+ # except if the file is an header without rom content
363
+ @roms.select {|rom| rom.has_content? && !rom.fshash.nil? }
364
+ .each {|rom|
365
+ hash = rom.fshash
366
+ destdir = dir
367
+ dirpart = case subdir
368
+ when nil, false then nil
369
+ when true then hash[0..3]
370
+ when Integer then hash[0..subdir]
371
+ when Proc then subdir.call(rom)
372
+ else raise ArgumentError, "unsupported subdir type"
373
+ end
374
+
375
+ if dirpart
376
+ # Update destination directory
377
+ destdir = File.join(destdir, *dirpart)
378
+ # Ensure destination directory exists
379
+ FileUtils.mkdir_p(destdir)
380
+ end
381
+
382
+ # Destination file
383
+ dest = File.join(destdir, hash)
384
+
385
+ # If the file exist, it is the right file, as it is
386
+ # named from it's hash (ie: content)
387
+ if force || !File.exists?(dest)
388
+ rom.copy(dest, part: part, force: force)
389
+ end
390
+
391
+ block.call(rom) if block
392
+ }
393
+ self
394
+ end
395
+
396
+
397
+ # Dumping of ROM vault entries
398
+ #
399
+ # @param compact [Boolean]
400
+ #
401
+ # @return [self]
402
+ #
403
+ # @yieldparam group [String]
404
+ # @yieldparam entries [Array<String>]
405
+ #
406
+ def dump(compact: false, &block)
407
+ self.each.inject({}) {|grp, rom|
408
+ grp.merge(rom.path.storage => [rom]) {|key, old, new| old + new }
409
+ }.each {|storage, roms|
410
+ size = if ROM::Path::Archive === roms.first.path
411
+ roms.first.path.archive.size
412
+ end
413
+
414
+ if storage.nil?
415
+ roms.each {|rom| block.call(rom.path.entry, nil) }
416
+ else
417
+ if compact && (size == roms.size)
418
+ then block.call(storage)
419
+ else block.call(storage, roms.map {|r| r.path.entry })
420
+ end
421
+ end
422
+ }
423
+ self
424
+ end
425
+
426
+ protected
427
+
428
+ def roms
429
+ @roms
430
+ end
431
+ end
432
+
433
+ end
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+
5
+ VERSION = "0.1"
6
+
7
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rom-distillery
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Stephane D'Alu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyzip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-screen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-logger
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-spinner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-progressbar
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Help organise emulation ROM using DAT file
126
+ email:
127
+ - sdalu@sdalu.com
128
+ executables:
129
+ - rhum
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - Gemfile
134
+ - LICENSE
135
+ - README.md
136
+ - bin/rhum
137
+ - distillery.gemspec
138
+ - lib/distillery.rb
139
+ - lib/distillery/archiver.rb
140
+ - lib/distillery/archiver/archive.rb
141
+ - lib/distillery/archiver/external.rb
142
+ - lib/distillery/archiver/external.yaml
143
+ - lib/distillery/archiver/libarchive.rb
144
+ - lib/distillery/archiver/zip.rb
145
+ - lib/distillery/cli.rb
146
+ - lib/distillery/cli/check.rb
147
+ - lib/distillery/cli/clean.rb
148
+ - lib/distillery/cli/header.rb
149
+ - lib/distillery/cli/index.rb
150
+ - lib/distillery/cli/overlap.rb
151
+ - lib/distillery/cli/rebuild.rb
152
+ - lib/distillery/cli/rename.rb
153
+ - lib/distillery/cli/repack.rb
154
+ - lib/distillery/cli/validate.rb
155
+ - lib/distillery/datfile.rb
156
+ - lib/distillery/error.rb
157
+ - lib/distillery/game.rb
158
+ - lib/distillery/game/release.rb
159
+ - lib/distillery/refinements.rb
160
+ - lib/distillery/rom-archive.rb
161
+ - lib/distillery/rom.rb
162
+ - lib/distillery/rom/path.rb
163
+ - lib/distillery/rom/path/archive.rb
164
+ - lib/distillery/rom/path/file.rb
165
+ - lib/distillery/rom/path/virtual.rb
166
+ - lib/distillery/storage.rb
167
+ - lib/distillery/vault.rb
168
+ - lib/distillery/version.rb
169
+ homepage: http://github.com/sdalu/distillery
170
+ licenses:
171
+ - EUPL-1.2
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '2.5'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.0.6
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: ROM manager
192
+ test_files: []