file-digests 0.0.20 → 0.0.25

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