file-digests 0.0.18 → 0.0.19

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/file-digests.rb +202 -211
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d69e75a52c05cbc2caf912491be9fbaaadb5136a2dfc723920a9010d3e4c2592
4
- data.tar.gz: f90deae82f2581d301d1cb7ba7c730e684b24f5c9582d6deb2581b9f2e6fa557
3
+ metadata.gz: 7aeebbfa688dc871d736ca71e740b3bdd4804701d29e4b960da16d1f75bdb04f
4
+ data.tar.gz: 136b2f908e41f56ba2ef243130d4a8a13b156f49b1c179a37225d3d19b0eff1c
5
5
  SHA512:
6
- metadata.gz: 5ad20e936d21d42f56ed20250728eb0b7c2b9a034877e764f528c6e297853bb7724d6780a56b7c0f11a3df975c1d51fed04852ddb3f3ff9848f4c546167902e8
7
- data.tar.gz: 27f86166310f420ac5858fa86c0233af666bbb2af40f53501a9f8e02205a3753151db5cfd6777558bbf0040e2472f9bc296d9382afebb8dd45f65fb981fc173b
6
+ metadata.gz: 9dd356eb305cd87874c3c8bc84905dec4388172e14db4b6a0d78004cf9bf183dc6c508537aa6f2806cc8d0f7eaeec68ccb6d05ea7cc5cf18db95fe86db9f4a15
7
+ data.tar.gz: 8a2d67681bc07b46fa4e93c280c8355eb639168e7bc1f244d3dae6cfd57e58fde6ded1a4d25aa2f63364bc639a55038936bedf990cc0608b267e2facd60bf556
@@ -5,7 +5,7 @@ require 'fileutils'
5
5
  require 'pathname'
6
6
  require 'sqlite3'
7
7
 
8
- module FileDigests
8
+ class FileDigests
9
9
 
10
10
  def self.perform_check
11
11
  options = {
@@ -13,271 +13,262 @@ module FileDigests
13
13
  quiet: (ENV["QUIET"] == "true"),
14
14
  test_only: (ENV["TEST_ONLY"] == "true")
15
15
  }
16
- checker = Checker.new ARGV[0], ARGV[1], options
17
- checker.perform_check
16
+ file_digests = self.new ARGV[0], ARGV[1], options
17
+ file_digests.perform_check
18
18
  end
19
19
 
20
- class DigestDatabase
21
- def initialize path, options = {}
22
- @options = options
23
-
24
- @db = SQLite3::Database.new path.to_s
25
- @db.results_as_hash = true
26
-
27
- execute 'PRAGMA journal_mode = "WAL"'
28
- execute 'PRAGMA synchronous = "NORMAL"'
29
- execute 'PRAGMA locking_mode = "EXCLUSIVE"'
30
- execute 'PRAGMA cache_size = "5000"'
31
-
32
- unless execute("SELECT name FROM sqlite_master WHERE type='table' AND name = 'digests'").length == 1
33
- execute 'PRAGMA encoding = "UTF-8"'
34
- execute "CREATE TABLE digests (
35
- id INTEGER PRIMARY KEY,
36
- filename TEXT,
37
- mtime TEXT,
38
- digest TEXT,
39
- digest_check_time TEXT)"
40
- execute "CREATE UNIQUE INDEX digests_filename ON digests(filename)"
41
- end
20
+ def initialize files_path, digest_database_path, options = {}
21
+ @options = options
22
+ @files_path = cleanup_path(files_path || ".")
23
+ @prefix_to_remove = @files_path.to_s + '/'
42
24
 
43
- @missing_files = Hash[@db.prepare("SELECT filename, digest FROM digests").execute!]
44
- @new_files = {}
25
+ raise "Files path must be a readable directory" unless (File.directory?(@files_path) && File.readable?(@files_path))
45
26
 
46
- prepare_method :insert, "INSERT INTO digests (filename, mtime, digest, digest_check_time) VALUES (?, ?, ?, datetime('now'))"
47
- prepare_method :find_by_filename, "SELECT id, mtime, digest FROM digests WHERE filename = ?"
48
- prepare_method :touch_digest_check_time, "UPDATE digests SET digest_check_time = datetime('now') WHERE id = ?"
49
- prepare_method :update_mtime_and_digest, "UPDATE digests SET mtime = ?, digest = ?, digest_check_time = datetime('now') WHERE id = ?"
50
- prepare_method :update_mtime, "UPDATE digests SET mtime = ?, digest_check_time = datetime('now') WHERE id = ?"
51
- prepare_method :delete_by_filename, "DELETE FROM digests WHERE filename = ?"
27
+ @digest_database_path = if digest_database_path
28
+ cleanup_path(digest_database_path)
29
+ else
30
+ @files_path + '.file-digests.sqlite'
52
31
  end
53
32
 
54
- def insert_or_update file_path, mtime, digest, counters
55
- result = find_by_filename file_path
56
-
57
- if found = result.next_hash
58
- raise "Multiple records found" if result.next
59
-
60
- @missing_files.delete(file_path)
61
-
62
- if found['digest'] == digest
63
- counters[:good] += 1
64
- # puts "GOOD: #{file_path}" unless @options[:quiet]
65
- unless @options[:test_only]
66
- if found['mtime'] == mtime
67
- touch_digest_check_time found['id']
68
- else
69
- update_mtime mtime, found['id']
70
- end
71
- end
72
- else
73
- if found['mtime'] == mtime # Digest is different and mtime is the same
74
- counters[:likely_damaged] += 1
75
- STDERR.puts "LIKELY DAMAGED: #{file_path}"
76
- else
77
- counters[:updated] += 1
78
- puts "UPDATED: #{file_path}" unless @options[:quiet]
79
- unless @options[:test_only]
80
- update_mtime_and_digest mtime, digest, found['id']
81
- end
82
- end
83
- end
84
- else
85
- counters[:new] += 1
86
- puts "NEW: #{file_path}" unless @options[:quiet]
87
- unless @options[:test_only]
88
- @new_files[file_path] = digest
89
- insert file_path, mtime, digest
90
- end
91
- end
33
+ if File.directory?(@digest_database_path)
34
+ @digest_database_path = @digest_database_path + '.file-digests.sqlite'
92
35
  end
93
36
 
94
- def track_renames counters
95
- @missing_files.delete_if do |filename, digest|
96
- if @new_files.value?(digest)
97
- counters[:renamed] += 1
98
- unless @options[:test_only]
99
- delete_by_filename filename
100
- end
101
- true
102
- end
103
- end
104
- counters[:missing] = @missing_files.length
37
+ if @files_path == @digest_database_path.dirname
38
+ @skip_file_digests_sqlite = true
105
39
  end
106
40
 
107
- def any_missing_files?
108
- @missing_files.length > 0
109
- end
41
+ ensure_dir_exists @digest_database_path.dirname
110
42
 
111
- def print_missing_files
112
- puts "\nMISSING FILES:"
113
- @missing_files.sort.to_h.each do |filename, digest|
114
- puts filename
115
- end
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
116
46
  end
117
47
 
118
- def remove_missing_files
119
- @db.transaction do
120
- @missing_files.each do |filename, digest|
121
- delete_by_filename filename
122
- end
123
- end
124
- end
125
-
126
- private
48
+ initialize_database @digest_database_path
127
49
 
128
- def execute *args, &block
129
- @db.execute *args, &block
130
- end
131
-
132
- def prepare_method name, query
133
- variable = "@#{name}"
134
- instance_variable_set(variable, @db.prepare(query))
135
- define_singleton_method name do |*args, &block|
136
- instance_variable_get(variable).execute(*args, &block)
137
- end
138
- end
50
+ @counters = {good: 0, updated: 0, new: 0, missing: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
139
51
  end
140
52
 
141
- class Checker
142
- def initialize files_path, digest_database_path, options = {}
143
- @options = options
144
- @files_path = cleanup_path(files_path || ".")
145
- @prefix_to_remove = @files_path.to_s + '/'
146
-
147
- raise "Files path must be a readable directory" unless (File.directory?(@files_path) && File.readable?(@files_path))
53
+ def initialize_database path
54
+ @db = SQLite3::Database.new path.to_s
55
+ @db.results_as_hash = true
56
+
57
+ execute 'PRAGMA journal_mode = "WAL"'
58
+ execute 'PRAGMA synchronous = "NORMAL"'
59
+ execute 'PRAGMA locking_mode = "EXCLUSIVE"'
60
+ execute 'PRAGMA cache_size = "5000"'
61
+
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
148
72
 
149
- @digest_database_path = if digest_database_path
150
- cleanup_path(digest_database_path)
151
- else
152
- @files_path + '.file-digests.sqlite'
153
- end
73
+ @missing_files = Hash[@db.prepare("SELECT filename, digest FROM digests").execute!]
74
+ @new_files = {}
154
75
 
155
- if File.directory?(@digest_database_path)
156
- @digest_database_path = @digest_database_path + '.file-digests.sqlite'
157
- end
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 = ?"
82
+ end
158
83
 
159
- if @files_path == @digest_database_path.dirname
160
- @skip_file_digests_sqlite = true
84
+ def perform_check
85
+ measure_time do
86
+ walk_files do |filename|
87
+ process_file filename
161
88
  end
89
+ end
162
90
 
163
- ensure_dir_exists @digest_database_path.dirname
91
+ track_renames
164
92
 
165
- # Please do not use this flag, support for sha512 is here for backward compatibility, and one day it will be removed.
166
- if File.exist?(@digest_database_path.dirname + '.file-digests.sha512')
167
- @use_sha512 = true
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
168
97
  end
98
+ end
169
99
 
170
- @digest_database = DigestDatabase.new @digest_database_path, @options
171
- @counters = {good: 0, updated: 0, new: 0, missing: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
100
+ if @counters[:likely_damaged] > 0 || @counters[:exceptions] > 0
101
+ STDERR.puts "ERRORS WERE OCCURRED"
172
102
  end
173
103
 
174
- def perform_check
175
- measure_time do
176
- walk_files do |filename|
177
- process_file filename
178
- end
179
- end
104
+ puts @counters.inspect
105
+ end
180
106
 
181
- @digest_database.track_renames @counters
107
+ private
182
108
 
183
- if @digest_database.any_missing_files?
184
- @digest_database.print_missing_files
185
- if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
186
- @digest_database.remove_missing_files
187
- end
188
- end
109
+ def process_file filename
110
+ return if File.symlink? filename
189
111
 
190
- if @counters[:likely_damaged] > 0 || @counters[:exceptions] > 0
191
- STDERR.puts "ERRORS WERE OCCURRED"
192
- end
112
+ stat = File.stat filename
193
113
 
194
- puts @counters.inspect
114
+ return if stat.blockdev?
115
+ return if stat.chardev?
116
+ return if stat.directory?
117
+ return if stat.pipe?
118
+ unless stat.readable?
119
+ raise "File is not readable"
195
120
  end
121
+ return if stat.socket?
122
+
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'
129
+ end
130
+
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
196
140
 
197
- private
141
+ def patch_path_string path
142
+ Gem.win_platform? ? path.gsub(/\\/, '/') : path
143
+ end
198
144
 
145
+ def cleanup_path path
146
+ Pathname.new(patch_path_string(path)).cleanpath
147
+ end
199
148
 
200
- def confirm text
201
- if STDIN.tty? && STDOUT.tty?
202
- puts "#{text} (y/n)?"
203
- STDIN.gets.strip.downcase == "y"
149
+ def ensure_dir_exists path
150
+ if File.exist?(path)
151
+ unless File.directory?(path)
152
+ raise "#{path} is not a directory"
204
153
  end
154
+ else
155
+ FileUtils.mkdir_p path
205
156
  end
157
+ end
206
158
 
207
- def process_file filename
208
- return if File.symlink? filename
209
-
210
- stat = File.stat filename
159
+ def walk_files
160
+ Dir.glob(@files_path + '**' + '*', File::FNM_DOTMATCH) do |filename|
161
+ yield filename
162
+ end
163
+ end
211
164
 
212
- return if stat.blockdev?
213
- return if stat.chardev?
214
- return if stat.directory?
215
- return if stat.pipe?
216
- unless stat.readable?
217
- raise "File is not readable"
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)
218
171
  end
219
- return if stat.socket?
220
-
221
- if @skip_file_digests_sqlite
222
- basename = File.basename(filename)
223
- return if basename == '.file-digests.sha512'
224
- return if basename == '.file-digests.sqlite'
225
- return if basename == '.file-digests.sqlite-wal'
226
- return if basename == '.file-digests.sqlite-shm'
227
- end
228
-
229
- @digest_database.insert_or_update(
230
- filename.delete_prefix(@prefix_to_remove).encode('utf-8', universal_newline: true).unicode_normalize(:nfkc),
231
- stat.mtime.utc.strftime('%Y-%m-%d %H:%M:%S'),
232
- get_file_digest(filename),
233
- @counters
234
- )
235
- rescue => exception
236
- @counters[:exceptions] += 1
237
- STDERR.puts "EXCEPTION: #{filename.encode('utf-8', universal_newline: true)}: #{exception.message}"
172
+ return digest.hexdigest
238
173
  end
174
+ end
239
175
 
240
- def patch_path_string path
241
- Gem.win_platform? ? path.gsub(/\\/, '/') : path
176
+ def confirm text
177
+ if STDIN.tty? && STDOUT.tty?
178
+ puts "#{text} (y/n)?"
179
+ STDIN.gets.strip.downcase == "y"
242
180
  end
181
+ end
243
182
 
244
- def cleanup_path path
245
- Pathname.new(patch_path_string(path)).cleanpath
246
- end
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
+
190
+ def insert_or_update file_path, mtime, digest
191
+ result = find_by_filename file_path
247
192
 
248
- def ensure_dir_exists path
249
- if File.exist?(path)
250
- unless File.directory?(path)
251
- raise "#{path} is not a directory"
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
252
207
  end
253
208
  else
254
- FileUtils.mkdir_p path
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
255
219
  end
256
- end
257
-
258
- def walk_files
259
- Dir.glob(@files_path + '**' + '*', File::FNM_DOTMATCH) do |filename|
260
- yield filename
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
261
226
  end
262
227
  end
228
+ end
263
229
 
264
- def get_file_digest filename
265
- File.open(filename, 'rb') do |io|
266
- digest = (@use_sha512 ? Digest::SHA512 : Digest::SHA256).new
267
- buffer = ""
268
- while io.read(40960, buffer)
269
- digest.update(buffer)
230
+ def track_renames
231
+ @missing_files.delete_if do |filename, digest|
232
+ if @new_files.value?(digest)
233
+ @counters[:renamed] += 1
234
+ unless @options[:test_only]
235
+ delete_by_filename filename
270
236
  end
271
- return digest.hexdigest
237
+ true
272
238
  end
273
239
  end
240
+ @counters[:missing] = @missing_files.length
241
+ end
274
242
 
275
- def measure_time
276
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
277
- yield
278
- elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).to_i
279
- puts "Elapsed time: #{elapsed / 3600}h #{(elapsed % 3600) / 60}m #{elapsed % 60}s" unless @options[:quiet]
243
+ def any_missing_files?
244
+ @missing_files.length > 0
245
+ end
246
+
247
+ def print_missing_files
248
+ puts "\nMISSING FILES:"
249
+ @missing_files.sort.to_h.each do |filename, digest|
250
+ puts filename
251
+ end
252
+ end
253
+
254
+ def remove_missing_files
255
+ @db.transaction do
256
+ @missing_files.each do |filename, digest|
257
+ delete_by_filename filename
258
+ end
280
259
  end
260
+ end
281
261
 
262
+ def execute *args, &block
263
+ @db.execute *args, &block
282
264
  end
265
+
266
+ def prepare_method name, query
267
+ variable = "@#{name}"
268
+ instance_variable_set(variable, @db.prepare(query))
269
+ define_singleton_method name do |*args, &block|
270
+ instance_variable_get(variable).execute(*args, &block)
271
+ end
272
+ end
273
+
283
274
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file-digests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.18
4
+ version: 0.0.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav Senotrusov