fingerprint 1.4.0 → 3.2.0

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.
@@ -0,0 +1,86 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ require_relative '../checker'
28
+ require_relative '../record'
29
+
30
+ module Fingerprint
31
+ module Command
32
+ class Verify < Samovar::Command
33
+ self.description = "Check an existing fingerprint against the filesystem."
34
+
35
+ options do
36
+ option "-n/--name <name>", "The fingerprint file name.", default: INDEX_FINGERPRINT
37
+
38
+ option "-f/--force", "Force all operations to complete despite warnings."
39
+ option "-x/--extended", "Include extended information about files and directories."
40
+
41
+ option "-s/--checksums <SHA2.256>", "Specify what checksum algorithms to use (#{Fingerprint::CHECKSUMS.keys.join(', ')}).", default: Fingerprint::DEFAULT_CHECKSUMS
42
+
43
+ option "--progress", "Print structured progress to standard error."
44
+ option "--verbose", "Verbose fingerprint output, e.g. excluded paths."
45
+
46
+ option "--fail-on-errors", "Exit with non-zero status if errors are encountered."
47
+ end
48
+
49
+ many :paths, "Paths relative to the root to use for verification, or ./ if not specified.", default: ["./"]
50
+
51
+ attr :error_count
52
+
53
+ def call
54
+ input_file = @options[:name]
55
+
56
+ unless File.exist? input_file
57
+ abort "Can't find index #{input_file}. Aborting."
58
+ end
59
+
60
+ options = @options.dup
61
+ options[:output] = @parent.output
62
+
63
+ master = RecordSet.new
64
+
65
+ File.open(input_file, "r") do |io|
66
+ master.parse(io)
67
+ end
68
+
69
+ if master.configuration
70
+ options.merge!(master.configuration.options)
71
+ end
72
+
73
+ scanner = Scanner.new(@paths, **options)
74
+
75
+ # We use a sparse record set here, so we can't check for additions.
76
+ copy = SparseRecordSet.new(scanner)
77
+
78
+ @error_count = Checker.verify(master, copy, **options)
79
+
80
+ if @options[:fail_on_errors]
81
+ abort "Data inconsistent, #{error_count} error(s) found!" if error_count != 0
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,90 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ require_relative 'scanner'
28
+
29
+ require_relative 'command/scan'
30
+ require_relative 'command/analyze'
31
+ require_relative 'command/verify'
32
+ require_relative 'command/compare'
33
+ require_relative 'command/duplicates'
34
+
35
+ module Fingerprint
36
+ module Command
37
+ def self.call(*args)
38
+ Top.call(*args)
39
+ end
40
+
41
+ class Top < Samovar::Command
42
+ self.description = "A file checksum analysis and verification tool."
43
+
44
+ options do
45
+ option '--root <path>', "Work in the given root directory."
46
+
47
+ option '-o/--output <path>', "Output the transcript to a specific file rather than stdout."
48
+
49
+ option '-h/--help', "Print out help information."
50
+ option '-v/--version', "Print out the application version."
51
+ end
52
+
53
+ def chdir(&block)
54
+ if root = @options[:root]
55
+ Dir.chdir(root, &block)
56
+ else
57
+ yield
58
+ end
59
+ end
60
+
61
+ def output
62
+ if path = @options[:output]
63
+ File.open(path, "w")
64
+ else
65
+ $stdout
66
+ end
67
+ end
68
+
69
+ nested :command, {
70
+ 'scan' => Scan,
71
+ 'analyze' => Analyze,
72
+ 'verify' => Verify,
73
+ 'compare' => Compare,
74
+ 'duplicates' => Duplicates
75
+ }, default: 'analyze'
76
+
77
+ def call
78
+ if @options[:version]
79
+ puts "fingerprint v#{VERSION}"
80
+ elsif @options[:help]
81
+ self.print_usage
82
+ else
83
+ chdir do
84
+ @command.call
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,6 +1,4 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
2
  #
5
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,42 +18,23 @@
20
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
19
  # THE SOFTWARE.
22
20
 
23
- require 'rubygems'
24
-
25
- require 'test/unit'
26
- require 'fileutils'
27
- require 'pathname'
28
- require 'fingerprint'
29
-
30
- require 'timeout'
31
-
32
- class TestFingerprint < Test::Unit::TestCase
33
- def test_analyze_verify
34
- File.open("junk.txt", "w") { |fp| fp.write("foobar") }
35
-
36
- result = system("fingerprint --analyze ./ -f")
37
-
38
- File.open("junk.txt", "w") { |fp| fp.write("foobar") }
39
-
40
- result = system("fingerprint --verify ./")
41
-
42
- assert_equal true, result
43
-
44
- File.open("junk.txt", "w") { |fp| fp.write("foobaz") }
45
-
46
- result = system("fingerprint -X --verify ./")
47
-
48
- assert_equal false, result
49
- end
21
+ require 'find'
22
+ require 'build/files/path'
23
+ require 'build/files/system'
50
24
 
51
- def test_check_paths
52
- errors = 0
53
- test_path = File.dirname(__FILE__)
54
-
55
- Fingerprint::check_paths(test_path, test_path) do |record, result, message|
56
- errors += 1
25
+ module Fingerprint
26
+ module Find
27
+ def self.find(root)
28
+ # Ensure root is a directory:
29
+ root += File::SEPARATOR unless root.end_with?(File::SEPARATOR)
30
+
31
+ ::Find.find(root) do |path|
32
+ yield Build::Files::Path.new(path, root)
33
+ end
57
34
  end
58
35
 
59
- assert_equal errors, 0
36
+ def self.prune
37
+ ::Find.prune
38
+ end
60
39
  end
61
40
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2011, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -25,6 +25,7 @@ module Fingerprint
25
25
  MODES = {
26
26
  :configuration => 'C',
27
27
  :file => 'F',
28
+ :link => 'L',
28
29
  :directory => 'D',
29
30
  :summary => 'S',
30
31
  :warning => 'W',
@@ -84,6 +85,16 @@ module Fingerprint
84
85
  end
85
86
 
86
87
  class RecordSet
88
+ def self.load_file(path)
89
+ File.open(path, "r") do |io|
90
+ self.load(io)
91
+ end
92
+ end
93
+
94
+ def self.load(io)
95
+ self.new.tap{|record_set| record_set.parse(io)}
96
+ end
97
+
87
98
  def initialize
88
99
  @records = []
89
100
  @paths = {}
@@ -115,6 +126,14 @@ module Fingerprint
115
126
  end
116
127
  end
117
128
 
129
+ def include?(path)
130
+ @paths.include?(path)
131
+ end
132
+
133
+ def empty?
134
+ @paths.empty?
135
+ end
136
+
118
137
  def lookup(path)
119
138
  return @paths[path]
120
139
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2011, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -19,28 +19,33 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'stringio'
22
- require 'find'
23
22
  require 'etc'
24
23
  require 'digest/sha2'
25
24
 
26
- module Fingerprint
25
+ require_relative 'find'
26
+ require_relative 'record'
27
+ require_relative 'version'
27
28
 
29
+ module Fingerprint
30
+ INDEX_FINGERPRINT = "index.fingerprint"
31
+
28
32
  CHECKSUMS = {
29
33
  'MD5' => lambda { Digest::MD5.new },
30
34
  'SHA1' => lambda { Digest::SHA1.new },
31
35
  'SHA2.256' => lambda { Digest::SHA2.new(256) },
36
+ 'SHA2.384' => lambda { Digest::SHA2.new(384) },
32
37
  'SHA2.512' => lambda { Digest::SHA2.new(512) },
33
38
  }
34
39
 
35
- DEFAULT_CHECKSUMS = ['MD5', 'SHA2.256']
40
+ DEFAULT_CHECKSUMS = ['SHA2.256']
36
41
 
37
42
  # The scanner class can scan a set of directories and produce an index.
38
43
  class Scanner
39
44
  # Initialize the scanner to scan a given set of directories in order.
40
45
  # [+options[:excludes]+] An array of regular expressions of files to avoid indexing.
41
46
  # [+options[:output]+] An +IO+ where the results will be written.
42
- def initialize(roots, options = {})
43
- @roots = roots
47
+ def initialize(roots, pwd: Dir.pwd, **options)
48
+ @roots = roots.collect{|root| File.expand_path(root, pwd)}
44
49
 
45
50
  @excludes = options[:excludes] || []
46
51
  @options = options
@@ -84,14 +89,14 @@ module Fingerprint
84
89
  end
85
90
 
86
91
  File.open(path, "rb") do |file|
87
- buf = ""
88
- while file.read(1024 * 1024 * 10, buf)
89
- total += buf.size
92
+ buffer = ""
93
+ while file.read(1024 * 1024 * 10, buffer)
94
+ total += buffer.bytesize
90
95
 
91
96
  @progress.call(total) if @progress
92
97
 
93
98
  @digests.each do |key, digest|
94
- digest << buf
99
+ digest << buffer
95
100
  end
96
101
  end
97
102
  end
@@ -106,35 +111,61 @@ module Fingerprint
106
111
  end
107
112
 
108
113
  def metadata_for(type, path)
109
- stat = File.stat(path)
110
114
  metadata = {}
115
+
116
+ if type == :link
117
+ metadata['file.symlink'] = File.readlink(path)
118
+ else
119
+ stat = File.stat(path)
120
+
121
+ if type == :file
122
+ metadata['file.size'] = stat.size
123
+ digests = digests_for(path)
124
+ metadata.merge!(digests)
125
+ elsif type == :blockdev or type == :chardev
126
+ metadata['file.dev_major'] = stat.dev_major
127
+ metadata['file.dev_minor'] = stat.dev_minor
128
+ end
111
129
 
112
- if type == :file
113
- metadata['file.size'] = stat.size
114
- digests = digests_for(path)
115
- metadata.merge!(digests)
116
- end
117
-
118
- # Extended information
119
- if @options[:extended]
120
- metadata['posix.time.modified'] = File.mtime(path)
130
+ # Extended information
131
+ if @options[:extended]
132
+ metadata['posix.time.modified'] = File.mtime(path)
121
133
 
122
- metadata['posix.mode'] = stat.mode.to_s(8)
134
+ metadata['posix.mode'] = stat.mode.to_s(8)
123
135
 
124
- metadata['posix.permissions.user.id'] = stat.uid
125
- metadata['posix.permissions.user.name'] = Etc.getpwuid(stat.uid).name
126
- metadata['posix.permissions.group.id'] = stat.gid
127
- metadata['posix.permissions.group.name'] = Etc.getgrgid(stat.gid).name
136
+ metadata['posix.permissions.user.id'] = stat.uid
137
+ metadata['posix.permissions.user.name'] = Etc.getpwuid(stat.uid).name
138
+ metadata['posix.permissions.group.id'] = stat.gid
139
+ metadata['posix.permissions.group.name'] = Etc.getgrgid(stat.gid).name
140
+ end
128
141
  end
129
-
142
+
130
143
  return metadata
131
144
  end
132
145
 
133
146
  # Output a directory header.
134
147
  def directory_record_for(path)
135
- Record.new(:directory, path, metadata_for(:directory, path))
148
+ Record.new(:directory, path.relative_path, metadata_for(:directory, path))
136
149
  end
137
150
 
151
+ def link_record_for(path)
152
+ metadata = metadata_for(:link, path)
153
+
154
+ Record.new(:link, path.relative_path, metadata)
155
+ end
156
+
157
+ def blockdev_record_for(path)
158
+ metadata = metadata_for(:blockdev, path)
159
+
160
+ Record.new(:blockdev, path.relative_path, metadata)
161
+ end
162
+
163
+ def chardev_record_for(path)
164
+ metadata = metadata_for(:chardev, path)
165
+
166
+ Record.new(:chardev, path.relative_path, metadata)
167
+ end
168
+
138
169
  # Output a file and associated metadata.
139
170
  def file_record_for(path)
140
171
  metadata = metadata_for(:file, path)
@@ -142,12 +173,30 @@ module Fingerprint
142
173
  # Should this be here or in metadata_for?
143
174
  # metadata.merge!(digests_for(path))
144
175
 
145
- Record.new(:file, path, metadata)
176
+ Record.new(:file, path.relative_path, metadata)
146
177
  end
147
178
 
148
179
  # Add information about excluded paths.
149
180
  def excluded_record_for(path)
150
- Record.new(:excluded, path)
181
+ Record.new(:excluded, path.relative_path)
182
+ end
183
+
184
+ def record_for(path)
185
+ stat = File.stat(path)
186
+
187
+ if stat.symlink?
188
+ return link_record_for(path)
189
+ elsif stat.blockdev?
190
+ return blockdev_record_for(path)
191
+ elsif stat.chardev?
192
+ return chardev_record_for(path)
193
+ elsif stat.socket?
194
+ return socket_record_for(path)
195
+ elsif stat.file?
196
+ return file_record_for(path)
197
+ end
198
+ rescue Errno::ENOENT
199
+ return nil
151
200
  end
152
201
 
153
202
  public
@@ -163,17 +212,13 @@ module Fingerprint
163
212
  return false
164
213
  end
165
214
 
166
- def valid_file?(path)
167
- !(excluded?(path) || File.symlink?(path) || !File.file?(path) || !File.readable?(path))
168
- end
169
-
170
215
  def scan_path(path)
216
+ return nil if excluded?(path)
217
+
171
218
  @roots.each do |root|
172
- Dir.chdir(root) do
173
- if valid_file?(path)
174
- return file_record_for(path)
175
- end
176
- end
219
+ full_path = Build::Files::Path.join(root, path)
220
+
221
+ return record_for(full_path)
177
222
  end
178
223
 
179
224
  return nil
@@ -192,23 +237,21 @@ module Fingerprint
192
237
  # Estimate the number of files and amount of data to process..
193
238
  if @options[:progress]
194
239
  @roots.each do |root|
195
- Dir.chdir(root) do
196
- Find.find("./") do |path|
197
- if @options[:progress]
198
- $stderr.puts "# Scanning: #{path}"
199
- end
200
-
201
- if File.directory?(path)
202
- if excluded?(path)
203
- Find.prune # Ignore this directory
204
- end
205
- else
206
- # Skip anything that isn't a valid file (e.g. pipes, sockets, symlinks).
207
- if valid_file?(path)
208
- total_count += 1
209
- total_size += File.size(path)
210
- end
211
- end
240
+ Find.find(root) do |path|
241
+ # Some special files fail here, and this was the simplest fix.
242
+ Find.prune unless File.exist?(path)
243
+
244
+ if @options[:progress]
245
+ $stderr.puts "# Scanning: #{path}"
246
+ end
247
+
248
+ if excluded?(path)
249
+ Find.prune if path.directory?
250
+ elsif path.symlink?
251
+ total_count += 1
252
+ elsif path.file?
253
+ total_count += 1
254
+ total_size += File.size(path)
212
255
  end
213
256
  end
214
257
  end
@@ -216,52 +259,52 @@ module Fingerprint
216
259
 
217
260
  if @options[:progress]
218
261
  @progress = lambda do |read_size|
219
- $stderr.puts "# Progress: File #{processed_count} / #{total_count}; Byte #{processed_size + read_size} / #{total_size} = #{sprintf('%0.3f%', (processed_size + read_size).to_f / total_size.to_f * 100.0)} (#{read_size}, #{processed_size}, #{total_size})"
262
+ $stderr.puts "# Progress: File #{processed_count} / #{total_count}; Byte #{processed_size + read_size} / #{total_size} = #{sprintf('%0.3f%%', (processed_size + read_size).to_f / total_size.to_f * 100.0)} (#{read_size}, #{processed_size}, #{total_size})"
220
263
  end
221
264
  end
222
265
 
223
266
  @roots.each do |root|
224
- Dir.chdir(root) do
225
- recordset << header_for(root)
267
+ recordset << header_for(root)
268
+
269
+ Find.find(root) do |path|
270
+ # Some special files fail here, and this was the simplest fix.
271
+ Find.prune unless File.exist?(path)
226
272
 
227
- Find.find("./") do |path|
228
- if @options[:progress]
229
- $stderr.puts "# Path: #{path}"
230
- end
273
+ if @options[:progress]
274
+ $stderr.puts "# Path: #{path.relative_path}"
275
+ end
276
+
277
+ if excluded?(path)
278
+ excluded_count += 1
231
279
 
232
- if File.directory?(path)
233
- if excluded?(path)
234
- excluded_count += 1
235
-
236
- if @options[:verbose]
237
- recordset << excluded_record_for(path)
238
- end
239
-
240
- Find.prune # Ignore this directory
241
- else
242
- directory_count += 1
243
-
244
- recordset << directory_record_for(path)
245
- end
246
- else
247
- # Skip anything that isn't a valid file (e.g. pipes, sockets, symlinks).
248
- if valid_file?(path)
249
- recordset << file_record_for(path)
250
-
251
- processed_count += 1
252
- processed_size += File.size(path)
253
- else
254
- excluded_count += 1
255
-
256
- if @options[:verbose]
257
- recordset << excluded_record_for(path)
258
- end
259
- end
280
+ if @options[:verbose]
281
+ recordset << excluded_record_for(path)
260
282
  end
261
283
 
262
- # Print out a progress summary if requested
263
- @progress.call(0) if @progress
284
+ Find.prune if path.directory?
285
+ elsif path.directory?
286
+ directory_count += 1
287
+
288
+ recordset << directory_record_for(path)
289
+ elsif path.symlink?
290
+ recordset << link_record_for(path)
291
+
292
+ processed_count += 1
293
+ elsif path.file?
294
+ recordset << file_record_for(path)
295
+
296
+ processed_count += 1
297
+ processed_size += File.size(path)
298
+ else
299
+ excluded_count += 1
300
+
301
+ if @options[:verbose]
302
+ recordset << excluded_record_for(path)
303
+ end
264
304
  end
305
+
306
+ # Print out a progress summary if requested
307
+ @progress.call(0) if @progress
265
308
  end
266
309
  end
267
310
 
@@ -280,7 +323,7 @@ module Fingerprint
280
323
  end
281
324
 
282
325
  # A helper function to scan a set of directories.
283
- def self.scan_paths(paths, options = {})
326
+ def self.scan_paths(paths, **options)
284
327
  if options[:output]
285
328
  if options.key? :recordset
286
329
  recordset = options[:recordset]
@@ -291,7 +334,7 @@ module Fingerprint
291
334
  options[:recordset] = RecordSetPrinter.new(recordset, options[:output])
292
335
  end
293
336
 
294
- scanner = Scanner.new(paths, options)
337
+ scanner = Scanner.new(paths, **options)
295
338
 
296
339
  scanner.scan(options[:recordset])
297
340
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2011, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -18,7 +18,6 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
-
22
21
  module Fingerprint
23
- VERSION = "1.4.0"
22
+ VERSION = "3.2.0"
24
23
  end
data/lib/fingerprint.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2011, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@ require 'fingerprint/checker'
25
25
 
26
26
  module Fingerprint
27
27
  # A helper function to check two paths for consistency. Provides callback from +Fingerprint::Checker+.
28
- def self.check_paths(master_path, copy_path, &block)
28
+ def self.check_paths(master_path, copy_path, **options, &block)
29
29
  master = Scanner.new([master_path])
30
30
  copy = Scanner.new([copy_path])
31
31
 
@@ -34,10 +34,23 @@ module Fingerprint
34
34
 
35
35
  master.scan(master_recordset)
36
36
 
37
- checker = Checker.new(master_recordset, copy_recordset)
37
+ checker = Checker.new(master_recordset, copy_recordset, **options)
38
38
 
39
39
  checker.check(&block)
40
40
 
41
41
  return checker
42
42
  end
43
+
44
+ # Returns true if the given paths contain identical files. Useful for expectations, e.g. `expect(Fingerprint).to be_identical(source, destination)`
45
+ def self.identical?(source, destination, &block)
46
+ failures = 0
47
+
48
+ check_paths(source, destination) do |record, name, message|
49
+ failures += 1
50
+
51
+ yield(record) if block_given?
52
+ end
53
+
54
+ return failures == 0
55
+ end
43
56
  end
data.tar.gz.sig ADDED
Binary file