file-digests 0.0.18 → 0.0.19

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