file-digests 0.0.21 → 0.0.26

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