file-digests 0.0.19 → 0.0.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aeebbfa688dc871d736ca71e740b3bdd4804701d29e4b960da16d1f75bdb04f
4
- data.tar.gz: 136b2f908e41f56ba2ef243130d4a8a13b156f49b1c179a37225d3d19b0eff1c
3
+ metadata.gz: 1816d453ddc057c7a8eef4a0c81f63699f9ba10fdee404488fa8dc945bac791d
4
+ data.tar.gz: ae5baf350ac64577f0807b81231f045d875eff6351c78e559df46379ad8d6b1e
5
5
  SHA512:
6
- metadata.gz: 9dd356eb305cd87874c3c8bc84905dec4388172e14db4b6a0d78004cf9bf183dc6c508537aa6f2806cc8d0f7eaeec68ccb6d05ea7cc5cf18db95fe86db9f4a15
7
- data.tar.gz: 8a2d67681bc07b46fa4e93c280c8355eb639168e7bc1f244d3dae6cfd57e58fde6ded1a4d25aa2f63364bc639a55038936bedf990cc0608b267e2facd60bf556
6
+ metadata.gz: d76b3e25d709e17e7260c0ab7836f016107c7335461f7d8ab6a74b391067fbfb1691abd6286fa3894aa432b9af229ba417972ba08c5d1524dd06d06c8dffe653
7
+ data.tar.gz: 9e39f4ae5d19268c986fac6d6d3bc47e2be66bb60857be608c033670ccd4b7ba3f0c5a8242a18e9ebaf161b8f5b230c040f931405e32a5a53d840e250c46fe9f
@@ -2,4 +2,4 @@
2
2
 
3
3
  require 'file-digests'
4
4
 
5
- FileDigests.perform_check
5
+ FileDigests.run_cli_utility
@@ -1,107 +1,260 @@
1
1
  require 'date'
2
- require 'set'
3
2
  require 'digest'
4
3
  require 'fileutils'
4
+ require 'openssl'
5
+ require 'optparse'
5
6
  require 'pathname'
7
+ require 'set'
6
8
  require 'sqlite3'
7
9
 
8
10
  class FileDigests
11
+ DIGEST_ALGORITHMS=["BLAKE2b512", "SHA3-256", "SHA512-256"]
12
+
13
+ def self.canonical_digest_algorithm_name(string)
14
+ if string
15
+ index = DIGEST_ALGORITHMS.map(&:downcase).index(string.downcase)
16
+ index && DIGEST_ALGORITHMS[index]
17
+ end
18
+ end
19
+
20
+ def canonical_digest_algorithm_name string
21
+ self.class.canonical_digest_algorithm_name string
22
+ end
23
+
24
+ def self.digest_algorithms_list_text
25
+ "Digest algorithm should be one of the following: #{DIGEST_ALGORITHMS.join ", "}"
26
+ end
27
+
28
+ def self.parse_cli_options
29
+ options = {}
30
+
31
+ OptionParser.new do |opts|
32
+ opts.banner = [
33
+ "Usage: file-digests [options] [path/to/directory] [path/to/database_file]",
34
+ " By default the current directory will be operated upon, and the database file will be placed to the current directory as well.",
35
+ " Should you wish to check current directory but place the database elsewhere, you could provide \".\" as a first argument, and the path to a database_file as a second."
36
+ ].join "\n"
37
+
38
+ opts.on("-a", "--auto", "Do not ask for any confirmation") do
39
+ options[:auto] = true
40
+ end
41
+
42
+ opts.on(
43
+ '--digest=DIGEST',
44
+ 'Select a digest algorithm to use. Default is "BLAKE2b512".',
45
+ 'You might also consider to use slower "SHA512-256" or even more slower "SHA3-256".',
46
+ "#{digest_algorithms_list_text}.",
47
+ 'You only need to specify an algorithm on the first run, your choice will be saved to a database.',
48
+ 'Any time later you could specify a new algorithm to change the current one.',
49
+ 'Transition to a new algorithm will only occur if all files pass the check by digests which were stored using the old one.'
50
+ ) do |value|
51
+ digest_algorithm = canonical_digest_algorithm_name(value)
52
+ unless digest_algorithm
53
+ STDERR.puts "ERROR: #{digest_algorithms_list_text}"
54
+ exit 1
55
+ end
56
+ options[:digest_algorithm] = digest_algorithm
57
+ end
58
+
59
+ opts.on("-d", "--duplicates", "Show the list of duplicate files, based on the information out of the database") do
60
+ options[:action] = :show_duplicates
61
+ end
62
+
63
+ opts.on("-t", "--test", "Perform only the test, do not modify the digest database") do
64
+ options[:test_only] = true
65
+ end
66
+
67
+ opts.on("-q", "--quiet", "Less verbose output, stil report any found issues") do
68
+ options[:quiet] = true
69
+ end
70
+
71
+ opts.on("-v", "--verbose", "More verbose output") do
72
+ options[:verbose] = true
73
+ end
74
+
75
+ opts.on("-h", "--help", "Prints this help") do
76
+ puts opts
77
+ exit
78
+ end
79
+ end.parse!
80
+ options
81
+ end
82
+
83
+ def self.run_cli_utility
84
+ options = parse_cli_options
9
85
 
10
- def self.perform_check
11
- options = {
12
- auto: (ENV["AUTO"] == "true"),
13
- quiet: (ENV["QUIET"] == "true"),
14
- test_only: (ENV["TEST_ONLY"] == "true")
15
- }
16
86
  file_digests = self.new ARGV[0], ARGV[1], options
17
- file_digests.perform_check
87
+ file_digests.send(options[:action] || :perform_check)
18
88
  end
19
89
 
20
90
  def initialize files_path, digest_database_path, options = {}
21
91
  @options = options
22
- @files_path = cleanup_path(files_path || ".")
23
- @prefix_to_remove = @files_path.to_s + '/'
24
92
 
25
- raise "Files path must be a readable directory" unless (File.directory?(@files_path) && File.readable?(@files_path))
93
+ initialize_paths files_path, digest_database_path
94
+ initialize_database
26
95
 
27
- @digest_database_path = if digest_database_path
28
- cleanup_path(digest_database_path)
96
+ if @digest_algorithm = canonical_digest_algorithm_name(get_metadata("digest_algorithm"))
97
+ if @options[:digest_algorithm] && @options[:digest_algorithm] != @digest_algorithm
98
+ @new_digest_algorithm = @options[:digest_algorithm]
99
+ end
29
100
  else
30
- @files_path + '.file-digests.sqlite'
101
+ @digest_algorithm = (@options[:digest_algorithm] || "BLAKE2b512")
102
+ set_metadata "digest_algorithm", @digest_algorithm
31
103
  end
32
104
 
33
- if File.directory?(@digest_database_path)
34
- @digest_database_path = @digest_database_path + '.file-digests.sqlite'
35
- end
105
+ puts "Using #{@digest_algorithm} digest algorithm" if @options[:verbose]
106
+ end
36
107
 
37
- if @files_path == @digest_database_path.dirname
38
- @skip_file_digests_sqlite = true
39
- end
108
+ def initialize_paths files_path, digest_database_path
109
+ @files_path = cleanup_path(files_path || ".")
40
110
 
111
+ raise "Files path must be a readable directory" unless (File.directory?(@files_path) && File.readable?(@files_path))
112
+
113
+ @digest_database_path = digest_database_path ? cleanup_path(digest_database_path) : @files_path
114
+ @digest_database_path += '.file-digests.sqlite' if File.directory?(@digest_database_path)
41
115
  ensure_dir_exists @digest_database_path.dirname
42
116
 
43
- # Please do not use this flag, support for sha512 is here for backward compatibility, and one day it will be removed.
44
- if File.exist?(@digest_database_path.dirname + '.file-digests.sha512')
45
- @use_sha512 = true
117
+ if @options[:verbose]
118
+ puts "Target directory: #{@files_path}"
119
+ puts "Database location: #{@digest_database_path}"
46
120
  end
47
-
48
- initialize_database @digest_database_path
49
-
50
- @counters = {good: 0, updated: 0, new: 0, missing: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
51
121
  end
52
122
 
53
- def initialize_database path
54
- @db = SQLite3::Database.new path.to_s
123
+ def initialize_database
124
+ @db = SQLite3::Database.new @digest_database_path.to_s
55
125
  @db.results_as_hash = true
56
126
 
127
+ file_digests_gem_version = Gem.loaded_specs["file-digests"]&.version&.to_s
128
+
129
+ execute 'PRAGMA encoding = "UTF-8"'
57
130
  execute 'PRAGMA journal_mode = "WAL"'
58
131
  execute 'PRAGMA synchronous = "NORMAL"'
59
132
  execute 'PRAGMA locking_mode = "EXCLUSIVE"'
60
133
  execute 'PRAGMA cache_size = "5000"'
61
134
 
62
- unless execute("SELECT name FROM sqlite_master WHERE type='table' AND name = 'digests'").length == 1
63
- execute 'PRAGMA encoding = "UTF-8"'
64
- execute "CREATE TABLE digests (
65
- id INTEGER PRIMARY KEY,
66
- filename TEXT,
67
- mtime TEXT,
68
- digest TEXT,
69
- digest_check_time TEXT)"
70
- execute "CREATE UNIQUE INDEX digests_filename ON digests(filename)"
71
- end
135
+ @db.transaction(:exclusive) do
136
+ metadata_table_was_created = false
137
+ unless table_exist?("metadata")
138
+ execute "CREATE TABLE metadata (
139
+ key TEXT NOT NULL PRIMARY KEY,
140
+ value TEXT)"
141
+ execute "CREATE UNIQUE INDEX metadata_key ON metadata(key)"
142
+ metadata_table_was_created = true
143
+ end
144
+
145
+ prepare_method :set_metadata_query, "INSERT INTO metadata (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value=excluded.value"
146
+ prepare_method :get_metadata_query, "SELECT value FROM metadata WHERE key = ?"
147
+
148
+ set_metadata("metadata_table_created_by_gem_version", file_digests_gem_version) if file_digests_gem_version && metadata_table_was_created
149
+
150
+ # Heuristic to detect database version 1 (metadata was not stored back then)
151
+ unless get_metadata("database_version")
152
+ if table_exist?("digests")
153
+ set_metadata "database_version", "1"
154
+ end
155
+ end
156
+
157
+ unless table_exist?("digests")
158
+ execute "CREATE TABLE digests (
159
+ id INTEGER NOT NULL PRIMARY KEY,
160
+ filename TEXT NOT NULL,
161
+ mtime TEXT,
162
+ digest TEXT NOT NULL,
163
+ digest_check_time TEXT NOT NULL)"
164
+ execute "CREATE UNIQUE INDEX digests_filename ON digests(filename)"
165
+ set_metadata("digests_table_created_by_gem_version", file_digests_gem_version) if file_digests_gem_version
166
+ end
72
167
 
73
- @missing_files = Hash[@db.prepare("SELECT filename, digest FROM digests").execute!]
74
- @new_files = {}
168
+ prepare_method :insert, "INSERT INTO digests (filename, mtime, digest, digest_check_time) VALUES (?, ?, ?, datetime('now'))"
169
+ prepare_method :find_by_filename_query, "SELECT id, mtime, digest FROM digests WHERE filename = ?"
170
+ prepare_method :touch_digest_check_time, "UPDATE digests SET digest_check_time = datetime('now') WHERE id = ?"
171
+ prepare_method :update_mtime_and_digest, "UPDATE digests SET mtime = ?, digest = ?, digest_check_time = datetime('now') WHERE id = ?"
172
+ prepare_method :update_mtime, "UPDATE digests SET mtime = ?, digest_check_time = datetime('now') WHERE id = ?"
173
+ prepare_method :delete_by_filename, "DELETE FROM digests WHERE filename = ?"
174
+ prepare_method :query_duplicates, "SELECT digest, filename FROM digests WHERE digest IN (SELECT digest FROM digests GROUP BY digest HAVING count(*) > 1) ORDER BY digest, filename;"
175
+ prepare_method :update_digest_to_new_digest, "UPDATE digests SET digest = ? WHERE digest = ?"
176
+
177
+ unless get_metadata("database_version")
178
+ set_metadata "database_version", "2"
179
+ end
180
+
181
+ # Convert database from 1st to 2nd version
182
+ unless get_metadata("digest_algorithm")
183
+ if get_metadata("database_version") == "1"
184
+ if File.exist?(@digest_database_path.dirname + '.file-digests.sha512')
185
+ set_metadata("digest_algorithm", "SHA512")
186
+ else
187
+ set_metadata("digest_algorithm", "SHA256")
188
+ end
189
+ set_metadata "database_version", "2"
190
+ end
191
+ end
75
192
 
76
- prepare_method :insert, "INSERT INTO digests (filename, mtime, digest, digest_check_time) VALUES (?, ?, ?, datetime('now'))"
77
- prepare_method :find_by_filename, "SELECT id, mtime, digest FROM digests WHERE filename = ?"
78
- prepare_method :touch_digest_check_time, "UPDATE digests SET digest_check_time = datetime('now') WHERE id = ?"
79
- prepare_method :update_mtime_and_digest, "UPDATE digests SET mtime = ?, digest = ?, digest_check_time = datetime('now') WHERE id = ?"
80
- prepare_method :update_mtime, "UPDATE digests SET mtime = ?, digest_check_time = datetime('now') WHERE id = ?"
81
- prepare_method :delete_by_filename, "DELETE FROM digests WHERE filename = ?"
193
+ if get_metadata("database_version") != "2"
194
+ STDERR.puts "This version of file-digests is only compartible with the database version 2. Current database version is #{get_metadata("database_version")}. To use this database, please install appropriate version if file-digest."
195
+ raise "Incompatible database version"
196
+ end
197
+ end
82
198
  end
83
199
 
84
200
  def perform_check
85
- measure_time do
86
- walk_files do |filename|
87
- process_file filename
201
+ perhaps_transaction(@new_digest_algorithm, :exclusive) do
202
+ @counters = {good: 0, updated: 0, new: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
203
+ @new_files = {}
204
+ @new_digests = {}
205
+
206
+ @missing_files = Hash[@db.prepare("SELECT filename, digest FROM digests").execute!]
207
+
208
+ measure_time do
209
+ walk_files do |filename|
210
+ process_file filename
211
+ end
88
212
  end
89
- end
90
213
 
91
- track_renames
214
+ track_renames
215
+
216
+ if any_missing_files?
217
+ if any_exceptions?
218
+ STDERR.puts "Due to previously occurred errors, database cleanup from missing files will be skipped this time."
219
+ else
220
+ print_missing_files
221
+ if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
222
+ remove_missing_files
223
+ end
224
+ end
225
+ end
92
226
 
93
- if any_missing_files?
94
- print_missing_files
95
- if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
96
- remove_missing_files
227
+ if @new_digest_algorithm && !@options[:test_only]
228
+ if any_missing_files? || any_likely_damaged? || any_exceptions?
229
+ STDERR.puts "ERROR: New digest algorithm will not be in effect until there are files that are missing, likely damaged, or processed with an exception."
230
+ else
231
+ @new_digests.each do |old_digest, new_digest|
232
+ update_digest_to_new_digest new_digest, old_digest
233
+ end
234
+ set_metadata "digest_algorithm", @new_digest_algorithm
235
+ end
236
+ end
237
+
238
+ if any_likely_damaged? || any_exceptions?
239
+ STDERR.puts "PLEASE REVIEW ERRORS THAT WERE OCCURRED!"
97
240
  end
98
- end
99
241
 
100
- if @counters[:likely_damaged] > 0 || @counters[:exceptions] > 0
101
- STDERR.puts "ERRORS WERE OCCURRED"
242
+ set_metadata(@options[:test_only] ? "latest_test_only_check_time" : "latest_complete_check_time", time_to_database(Time.now))
243
+
244
+ print_counters
102
245
  end
246
+ end
103
247
 
104
- puts @counters.inspect
248
+ def show_duplicates
249
+ current_digest = nil
250
+ query_duplicates.each do |found|
251
+ if current_digest != found['digest']
252
+ puts "" if current_digest
253
+ current_digest = found['digest']
254
+ puts "#{found['digest']}:"
255
+ end
256
+ puts " #{found['filename']}"
257
+ end
105
258
  end
106
259
 
107
260
  private
@@ -115,117 +268,72 @@ class FileDigests
115
268
  return if stat.chardev?
116
269
  return if stat.directory?
117
270
  return if stat.pipe?
118
- unless stat.readable?
119
- raise "File is not readable"
120
- end
121
271
  return if stat.socket?
122
272
 
123
- if @skip_file_digests_sqlite
124
- basename = File.basename(filename)
125
- return if basename == '.file-digests.sha512'
126
- return if basename == '.file-digests.sqlite'
127
- return if basename == '.file-digests.sqlite-wal'
128
- return if basename == '.file-digests.sqlite-shm'
273
+ raise "File is not readable" unless stat.readable?
274
+
275
+ if filename == "#{@digest_database_path}" ||
276
+ filename == "#{@digest_database_path}-wal" ||
277
+ filename == "#{@digest_database_path}-shm"
278
+ puts "SKIPPING DATABASE FILE: #{filename}" if @options[:verbose]
279
+ return
129
280
  end
130
281
 
131
- insert_or_update(
132
- filename.delete_prefix(@prefix_to_remove).encode('utf-8', universal_newline: true).unicode_normalize(:nfkc),
133
- stat.mtime.utc.strftime('%Y-%m-%d %H:%M:%S'),
134
- get_file_digest(filename)
135
- )
136
- rescue => exception
137
- @counters[:exceptions] += 1
138
- STDERR.puts "EXCEPTION: #{filename.encode('utf-8', universal_newline: true)}: #{exception.message}"
139
- end
282
+ normalized_filename = filename.delete_prefix("#{@files_path.to_s}/").encode('utf-8', universal_newline: true).unicode_normalize(:nfkc)
283
+ mtime_string = time_to_database stat.mtime
140
284
 
141
- def patch_path_string path
142
- Gem.win_platform? ? path.gsub(/\\/, '/') : path
143
- end
285
+ process_file_indeed normalized_filename, mtime_string, get_file_digest(filename)
144
286
 
145
- def cleanup_path path
146
- Pathname.new(patch_path_string(path)).cleanpath
287
+ rescue => exception
288
+ @counters[:exceptions] += 1
289
+ print_file_exception exception, filename
147
290
  end
148
291
 
149
- def ensure_dir_exists path
150
- if File.exist?(path)
151
- unless File.directory?(path)
152
- raise "#{path} is not a directory"
153
- end
292
+ def process_file_indeed filename, mtime, digest
293
+ if found = find_by_filename(filename)
294
+ process_previously_seen_file found, filename, mtime, digest
154
295
  else
155
- FileUtils.mkdir_p path
156
- end
157
- end
158
-
159
- def walk_files
160
- Dir.glob(@files_path + '**' + '*', File::FNM_DOTMATCH) do |filename|
161
- yield filename
296
+ process_new_file filename, mtime, digest
162
297
  end
163
298
  end
164
299
 
165
- def get_file_digest filename
166
- File.open(filename, 'rb') do |io|
167
- digest = (@use_sha512 ? Digest::SHA512 : Digest::SHA256).new
168
- buffer = ""
169
- while io.read(40960, buffer)
170
- digest.update(buffer)
300
+ def process_previously_seen_file found, filename, mtime, digest
301
+ @missing_files.delete(filename)
302
+ if found['digest'] == digest
303
+ @counters[:good] += 1
304
+ puts "GOOD: #{filename}" if @options[:verbose]
305
+ unless @options[:test_only]
306
+ if found['mtime'] == mtime
307
+ touch_digest_check_time found['id']
308
+ else
309
+ update_mtime mtime, found['id']
310
+ end
311
+ end
312
+ else
313
+ if found['mtime'] == mtime # Digest is different and mtime is the same
314
+ @counters[:likely_damaged] += 1
315
+ STDERR.puts "LIKELY DAMAGED: #{filename}"
316
+ else
317
+ @counters[:updated] += 1
318
+ puts "UPDATED: #{filename}" unless @options[:quiet]
319
+ unless @options[:test_only]
320
+ update_mtime_and_digest mtime, digest, found['id']
321
+ end
171
322
  end
172
- return digest.hexdigest
173
323
  end
174
324
  end
175
325
 
176
- def confirm text
177
- if STDIN.tty? && STDOUT.tty?
178
- puts "#{text} (y/n)?"
179
- STDIN.gets.strip.downcase == "y"
326
+ def process_new_file filename, mtime, digest
327
+ @counters[:new] += 1
328
+ puts "NEW: #{filename}" unless @options[:quiet]
329
+ unless @options[:test_only]
330
+ @new_files[filename] = digest
331
+ insert filename, mtime, digest
180
332
  end
181
333
  end
182
334
 
183
- def measure_time
184
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
185
- yield
186
- elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).to_i
187
- puts "Elapsed time: #{elapsed / 3600}h #{(elapsed % 3600) / 60}m #{elapsed % 60}s" unless @options[:quiet]
188
- end
189
335
 
190
- def insert_or_update file_path, mtime, digest
191
- result = find_by_filename file_path
192
-
193
- if found = result.next_hash
194
- raise "Multiple records found" if result.next
195
-
196
- @missing_files.delete(file_path)
197
-
198
- if found['digest'] == digest
199
- @counters[:good] += 1
200
- # puts "GOOD: #{file_path}" unless @options[:quiet]
201
- unless @options[:test_only]
202
- if found['mtime'] == mtime
203
- touch_digest_check_time found['id']
204
- else
205
- update_mtime mtime, found['id']
206
- end
207
- end
208
- else
209
- if found['mtime'] == mtime # Digest is different and mtime is the same
210
- @counters[:likely_damaged] += 1
211
- STDERR.puts "LIKELY DAMAGED: #{file_path}"
212
- else
213
- @counters[:updated] += 1
214
- puts "UPDATED: #{file_path}" unless @options[:quiet]
215
- unless @options[:test_only]
216
- update_mtime_and_digest mtime, digest, found['id']
217
- end
218
- end
219
- end
220
- else
221
- @counters[:new] += 1
222
- puts "NEW: #{file_path}" unless @options[:quiet]
223
- unless @options[:test_only]
224
- @new_files[file_path] = digest
225
- insert file_path, mtime, digest
226
- end
227
- end
228
- end
336
+ # Renames and missing files
229
337
 
230
338
  def track_renames
231
339
  @missing_files.delete_if do |filename, digest|
@@ -237,11 +345,6 @@ class FileDigests
237
345
  true
238
346
  end
239
347
  end
240
- @counters[:missing] = @missing_files.length
241
- end
242
-
243
- def any_missing_files?
244
- @missing_files.length > 0
245
348
  end
246
349
 
247
350
  def print_missing_files
@@ -252,23 +355,173 @@ class FileDigests
252
355
  end
253
356
 
254
357
  def remove_missing_files
255
- @db.transaction do
358
+ nested_transaction do
256
359
  @missing_files.each do |filename, digest|
257
360
  delete_by_filename filename
258
361
  end
362
+ @missing_files = {}
259
363
  end
260
364
  end
261
365
 
366
+
367
+ # Database helpers
368
+
262
369
  def execute *args, &block
263
370
  @db.execute *args, &block
264
371
  end
265
372
 
373
+ def nested_transaction(mode)
374
+ if @db.transaction_active?
375
+ yield
376
+ else
377
+ @db.transaction(mode) do
378
+ yield
379
+ end
380
+ end
381
+ end
382
+
383
+ def perhaps_transaction(condition, mode)
384
+ if condition
385
+ @db.transaction(mode) do
386
+ yield
387
+ end
388
+ else
389
+ yield
390
+ end
391
+ end
392
+
393
+ def table_exist? table_name
394
+ execute("SELECT name FROM sqlite_master WHERE type='table' AND name = '#{table_name}'").length == 1
395
+ end
396
+
266
397
  def prepare_method name, query
267
398
  variable = "@#{name}"
399
+
268
400
  instance_variable_set(variable, @db.prepare(query))
401
+
269
402
  define_singleton_method name do |*args, &block|
270
403
  instance_variable_get(variable).execute(*args, &block)
271
404
  end
405
+
406
+ define_singleton_method "#{name}!" do |*args, &block|
407
+ instance_variable_get(variable).execute!(*args, &block)
408
+ end
409
+ end
410
+
411
+ def set_metadata key, value
412
+ set_metadata_query key, value
413
+ puts "#{key} set to: #{value}" if @options[:verbose]
414
+ value
415
+ end
416
+
417
+ def get_metadata key
418
+ get_metadata_query!(key)&.first&.first
419
+ end
420
+
421
+ def find_by_filename filename
422
+ result = find_by_filename_query filename
423
+ found = result.next
424
+ raise "Multiple records found" if result.next
425
+ found
426
+ end
427
+
428
+ def time_to_database time
429
+ time.utc.strftime('%Y-%m-%d %H:%M:%S')
430
+ end
431
+
432
+
433
+ # Filesystem-related helpers
434
+
435
+ def patch_path_string path
436
+ Gem.win_platform? ? path.gsub(/\\/, '/') : path
437
+ end
438
+
439
+ def cleanup_path path
440
+ Pathname.new(patch_path_string(path)).cleanpath
441
+ end
442
+
443
+ def ensure_dir_exists path
444
+ if File.exist?(path)
445
+ unless File.directory?(path)
446
+ raise "#{path} is not a directory"
447
+ end
448
+ else
449
+ FileUtils.mkdir_p path
450
+ end
272
451
  end
273
452
 
453
+ def walk_files
454
+ Dir.glob(@files_path + '**' + '*', File::FNM_DOTMATCH) do |filename|
455
+ yield filename
456
+ end
457
+ end
458
+
459
+ def get_file_digest filename
460
+ File.open(filename, 'rb') do |io|
461
+ digest = OpenSSL::Digest.new(@digest_algorithm)
462
+ new_digest = OpenSSL::Digest.new(@new_digest_algorithm) if @new_digest_algorithm
463
+
464
+ buffer = ""
465
+ while io.read(409600, buffer) # 409600 seems like a sweet spot
466
+ digest.update(buffer)
467
+ new_digest.update(buffer) if @new_digest_algorithm
468
+ end
469
+ @new_digests[digest.hexdigest] = new_digest.hexdigest if @new_digest_algorithm
470
+ return digest.hexdigest
471
+ end
472
+ end
473
+
474
+
475
+ # Runtime state helpers
476
+
477
+ def any_missing_files?
478
+ @missing_files.length > 0
479
+ end
480
+
481
+ def any_exceptions?
482
+ @counters[:exceptions] > 0
483
+ end
484
+
485
+ def any_likely_damaged?
486
+ @counters[:likely_damaged] > 0
487
+ end
488
+
489
+ # UI helpers
490
+
491
+ def confirm text
492
+ if STDIN.tty? && STDOUT.tty?
493
+ puts "#{text} (y/n)?"
494
+ STDIN.gets.strip.downcase == "y"
495
+ end
496
+ end
497
+
498
+ def measure_time
499
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
500
+ yield
501
+ elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
502
+ puts "Elapsed time: #{elapsed.to_i / 3600}h #{(elapsed.to_i % 3600) / 60}m #{'%.3f' % (elapsed % 60)}s" unless @options[:quiet]
503
+ end
504
+
505
+ def print_file_exception exception, filename
506
+ STDERR.print "EXCEPTION: #{exception.message}, processing file: "
507
+ begin
508
+ STDERR.print filename.encode('utf-8', universal_newline: true)
509
+ rescue
510
+ STDERR.print "(Unable to encode file name to utf-8) "
511
+ STDERR.print filename
512
+ end
513
+ STDERR.print "\n"
514
+ STDERR.flush
515
+ exception.backtrace.each { |line| STDERR.puts " " + line }
516
+ end
517
+
518
+ def print_counters
519
+ puts "#{@counters[:good]} file(s) passes digest check" if @counters[:good] > 0
520
+ puts "#{@counters[:updated]} file(s) are updated" if @counters[:updated] > 0
521
+ puts "#{@counters[:new]} file(s) are new" if @counters[:new] > 0
522
+ puts "#{@counters[:renamed]} file(s) are renamed" if @counters[:renamed] > 0
523
+ puts "#{@missing_files.length} file(s) are missing" if @missing_files.length > 0
524
+ puts "#{@counters[:likely_damaged]} file(s) are likely damaged (!)" if @counters[:likely_damaged] > 0
525
+ puts "#{@counters[:exceptions]} file(s) had exceptions occured during processing (!)" if @counters[:exceptions] > 0
526
+ end
274
527
  end
metadata CHANGED
@@ -1,47 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file-digests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav Senotrusov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-08 00:00:00.000000000 Z
11
+ date: 2020-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.0
19
+ version: '1.3'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.3.0
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
27
41
  description: Calculate file digests and check for the possible file corruption
28
42
  email: stan@senotrusov.com
29
43
  executables:
30
44
  - file-digests
31
- - file-digests-auto
32
- - file-digests-test
33
45
  extensions: []
34
46
  extra_rdoc_files: []
35
47
  files:
36
48
  - bin/file-digests
37
- - bin/file-digests-auto
38
- - bin/file-digests-test
39
49
  - lib/file-digests.rb
40
50
  homepage: https://github.com/senotrusov/file-digests
41
51
  licenses:
42
52
  - Apache-2.0
43
53
  metadata: {}
44
- post_install_message:
54
+ post_install_message:
45
55
  rdoc_options: []
46
56
  require_paths:
47
57
  - lib
@@ -57,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
67
  version: '0'
58
68
  requirements: []
59
69
  rubygems_version: 3.1.2
60
- signing_key:
70
+ signing_key:
61
71
  specification_version: 4
62
72
  summary: file-digests
63
73
  test_files: []
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- ENV["AUTO"] = "true"
4
-
5
- require 'file-digests'
6
-
7
- FileDigests.perform_check
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- ENV["TEST_ONLY"] = "true"
4
-
5
- require 'file-digests'
6
-
7
- FileDigests.perform_check