file-digests 0.0.20 → 0.0.25

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: 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