file-digests 0.0.19 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
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