file-digests 0.0.33 → 0.0.34
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/file-digests +14 -0
- data/lib/file-digests.rb +154 -96
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd11bc930ad2146b6e9bbecd44479df1817c8262197a184f19d5db03296d16c1
|
4
|
+
data.tar.gz: 60a3741b3b6d7cfa714991bac995176398e4834a53106751b55d537a9e9ac901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cfbe4ac1169e5b48c9bb0c1ee3200fa060e38bc770200d14a127f98ab3967f83476f35130f4bd9d8427bbe7cac07c33b43e14c086e5897921c7383f0748cbfe
|
7
|
+
data.tar.gz: 9a5830dc67eb127ff39a0fffda53193d5ad43cfe25405e56d7bd211475af69192f404433c02f2c83d0c3f79dd4a56e4a02f469dbaad52abf6ab2027d8f9894bf
|
data/bin/file-digests
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
# Copyright 2020 Stanislav Senotrusov <stan@senotrusov.com>
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
3
17
|
require 'file-digests'
|
4
18
|
|
5
19
|
FileDigests.run_cli_utility
|
data/lib/file-digests.rb
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
# Copyright 2020 Stanislav Senotrusov <stan@senotrusov.com>
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
1
15
|
require "date"
|
2
16
|
require "digest"
|
3
17
|
require "fileutils"
|
@@ -8,7 +22,8 @@ require "set"
|
|
8
22
|
require "sqlite3"
|
9
23
|
|
10
24
|
class FileDigests
|
11
|
-
|
25
|
+
VERSION = Gem.loaded_specs["file-digests"]&.version&.to_s
|
26
|
+
DIGEST_ALGORITHMS = ["BLAKE2b512", "SHA3-256", "SHA512-256"]
|
12
27
|
LEGACY_DIGEST_ALGORITHMS = ["SHA512", "SHA256"]
|
13
28
|
|
14
29
|
def self.canonical_digest_algorithm_name(string)
|
@@ -101,6 +116,7 @@ class FileDigests
|
|
101
116
|
|
102
117
|
def initialize files_path, digest_database_path, options = {}
|
103
118
|
@options = options
|
119
|
+
@user_input_wait_time = 0
|
104
120
|
|
105
121
|
initialize_paths files_path, digest_database_path
|
106
122
|
initialize_database
|
@@ -144,8 +160,6 @@ class FileDigests
|
|
144
160
|
@db.results_as_hash = true
|
145
161
|
@db.busy_timeout = 5000
|
146
162
|
|
147
|
-
file_digests_gem_version = Gem.loaded_specs["file-digests"]&.version&.to_s
|
148
|
-
|
149
163
|
execute "PRAGMA encoding = 'UTF-8'"
|
150
164
|
execute "PRAGMA locking_mode = 'EXCLUSIVE'"
|
151
165
|
execute "PRAGMA journal_mode = 'WAL'"
|
@@ -160,14 +174,13 @@ class FileDigests
|
|
160
174
|
execute "CREATE TABLE metadata (
|
161
175
|
key TEXT NOT NULL PRIMARY KEY,
|
162
176
|
value TEXT)"
|
163
|
-
execute "CREATE UNIQUE INDEX metadata_key ON metadata(key)"
|
164
177
|
metadata_table_was_created = true
|
165
178
|
end
|
166
179
|
|
167
180
|
prepare_method :set_metadata_query, "INSERT INTO metadata (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value=excluded.value"
|
168
181
|
prepare_method :get_metadata_query, "SELECT value FROM metadata WHERE key = ?"
|
169
182
|
|
170
|
-
set_metadata("metadata_table_created_by_gem_version",
|
183
|
+
set_metadata("metadata_table_created_by_gem_version", FileDigests::VERSION) if FileDigests::VERSION && metadata_table_was_created
|
171
184
|
|
172
185
|
# Heuristic to detect database version 1 (metadata was not stored back then)
|
173
186
|
unless get_metadata("database_version")
|
@@ -184,20 +197,19 @@ class FileDigests
|
|
184
197
|
digest TEXT NOT NULL,
|
185
198
|
digest_check_time TEXT NOT NULL)"
|
186
199
|
execute "CREATE UNIQUE INDEX digests_filename ON digests(filename)"
|
187
|
-
|
200
|
+
execute "CREATE INDEX digests_digest ON digests(digest)"
|
201
|
+
set_metadata("digests_table_created_by_gem_version", FileDigests::VERSION) if FileDigests::VERSION
|
188
202
|
end
|
189
203
|
|
190
|
-
prepare_method :
|
191
|
-
prepare_method :
|
192
|
-
prepare_method :
|
193
|
-
prepare_method :
|
194
|
-
prepare_method :
|
195
|
-
prepare_method :
|
196
|
-
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;"
|
197
|
-
prepare_method :update_digest_to_new_digest, "UPDATE digests SET digest = ? WHERE digest = ?"
|
204
|
+
prepare_method :digests_insert, "INSERT INTO digests (filename, mtime, digest, digest_check_time) VALUES (?, ?, ?, datetime('now'))"
|
205
|
+
prepare_method :digests_find_by_filename_query, "SELECT id, mtime, digest FROM digests WHERE filename = ?"
|
206
|
+
prepare_method :digests_touch_check_time, "UPDATE digests SET digest_check_time = datetime('now') WHERE id = ?"
|
207
|
+
prepare_method :digests_update_mtime_and_digest, "UPDATE digests SET mtime = ?, digest = ?, digest_check_time = datetime('now') WHERE id = ?"
|
208
|
+
prepare_method :digests_update_mtime, "UPDATE digests SET mtime = ?, digest_check_time = datetime('now') WHERE id = ?"
|
209
|
+
prepare_method :digests_select_duplicates, "SELECT digest, filename FROM digests WHERE digest IN (SELECT digest FROM digests GROUP BY digest HAVING count(*) > 1) ORDER BY digest, filename;"
|
198
210
|
|
199
211
|
unless get_metadata("database_version")
|
200
|
-
set_metadata "database_version", "
|
212
|
+
set_metadata "database_version", "3"
|
201
213
|
end
|
202
214
|
|
203
215
|
# Convert database from 1st to 2nd version
|
@@ -212,79 +224,110 @@ class FileDigests
|
|
212
224
|
end
|
213
225
|
end
|
214
226
|
|
215
|
-
if get_metadata("database_version")
|
216
|
-
|
217
|
-
|
227
|
+
if get_metadata("database_version") == "2"
|
228
|
+
execute "CREATE INDEX digests_digest ON digests(digest)"
|
229
|
+
set_metadata "database_version", "3"
|
218
230
|
end
|
231
|
+
|
232
|
+
check_if_database_is_at_certain_version "3"
|
233
|
+
|
234
|
+
create_temporary_tables
|
219
235
|
end
|
220
236
|
end
|
221
237
|
|
222
|
-
def
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
238
|
+
def create_temporary_tables
|
239
|
+
execute "CREATE TEMPORARY TABLE new_files (
|
240
|
+
filename TEXT NOT NULL PRIMARY KEY,
|
241
|
+
digest TEXT NOT NULL)"
|
242
|
+
execute "CREATE INDEX new_files_digest ON new_files(digest)"
|
243
|
+
|
244
|
+
prepare_method :new_files_insert, "INSERT INTO new_files (filename, digest) VALUES (?, ?)"
|
245
|
+
prepare_method :new_files_count_query, "SELECT count(*) FROM new_files"
|
246
|
+
|
247
|
+
execute "CREATE TEMPORARY TABLE missing_files (
|
248
|
+
filename TEXT NOT NULL PRIMARY KEY,
|
249
|
+
digest TEXT NOT NULL)"
|
250
|
+
execute "CREATE INDEX missing_files_digest ON missing_files(digest)"
|
227
251
|
|
228
|
-
|
252
|
+
execute "INSERT INTO missing_files (filename, digest) SELECT filename, digest FROM digests"
|
253
|
+
|
254
|
+
prepare_method :missing_files_delete, "DELETE FROM missing_files WHERE filename = ?"
|
255
|
+
prepare_method :missing_files_delete_renamed_files, "DELETE FROM missing_files WHERE digest IN (SELECT digest FROM new_files)"
|
256
|
+
prepare_method :missing_files_select_all_filenames, "SELECT filename FROM missing_files ORDER BY filename"
|
257
|
+
prepare_method :missing_files_delete_all, "DELETE FROM missing_files"
|
258
|
+
prepare_method :missing_files_count_query, "SELECT count(*) FROM missing_files"
|
259
|
+
|
260
|
+
prepare_method :digests_delete_renamed_files, "DELETE FROM digests WHERE filename IN (SELECT filename FROM missing_files WHERE digest IN (SELECT digest FROM new_files))"
|
261
|
+
prepare_method :digests_delete_all_missing_files, "DELETE FROM digests WHERE filename IN (SELECT filename FROM missing_files)"
|
262
|
+
|
263
|
+
execute "CREATE TEMPORARY TABLE new_digests (
|
264
|
+
filename TEXT NOT NULL PRIMARY KEY,
|
265
|
+
digest TEXT NOT NULL)"
|
266
|
+
|
267
|
+
prepare_method :new_digests_insert, "INSERT INTO new_digests (filename, digest) VALUES (?, ?)"
|
268
|
+
prepare_method :digests_update_digests_to_new_digests, "INSERT INTO digests (filename, digest, digest_check_time) SELECT filename, digest, false FROM new_digests WHERE true ON CONFLICT (filename) DO UPDATE SET digest=excluded.digest"
|
269
|
+
end
|
270
|
+
|
271
|
+
def perform_check
|
272
|
+
measure_time do
|
273
|
+
perhaps_transaction(@new_digest_algorithm, :exclusive) do
|
274
|
+
@counters = {good: 0, updated: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
|
229
275
|
|
230
|
-
measure_time do
|
231
276
|
walk_files do |filename|
|
232
277
|
process_file filename
|
233
278
|
end
|
234
|
-
end
|
235
279
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
280
|
+
nested_transaction do
|
281
|
+
puts "Tracking renames..." if @options[:verbose]
|
282
|
+
track_renames
|
283
|
+
end
|
240
284
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
285
|
+
if any_missing_files?
|
286
|
+
if any_exceptions?
|
287
|
+
STDERR.puts "Due to previously occurred errors, missing files will not removed from the database."
|
288
|
+
else
|
289
|
+
print_missing_files
|
290
|
+
if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
|
291
|
+
nested_transaction do
|
292
|
+
puts "Removing missing files..." if @options[:verbose]
|
293
|
+
remove_missing_files
|
294
|
+
end
|
250
295
|
end
|
251
296
|
end
|
252
297
|
end
|
253
|
-
end
|
254
298
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
299
|
+
if @new_digest_algorithm && !@options[:test_only]
|
300
|
+
if any_missing_files? || any_likely_damaged? || any_exceptions?
|
301
|
+
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."
|
302
|
+
else
|
303
|
+
puts "Updating database to a new digest algorithm..." if @options[:verbose]
|
304
|
+
digests_update_digests_to_new_digests
|
305
|
+
set_metadata "digest_algorithm", @new_digest_algorithm
|
306
|
+
puts "Transition to a new digest algorithm complete: #{@new_digest_algorithm}"
|
262
307
|
end
|
263
|
-
set_metadata "digest_algorithm", @new_digest_algorithm
|
264
|
-
puts "Transition to a new digest algorithm complete: #{@new_digest_algorithm}"
|
265
308
|
end
|
266
|
-
end
|
267
309
|
|
268
|
-
|
269
|
-
|
270
|
-
|
310
|
+
if any_likely_damaged? || any_exceptions?
|
311
|
+
STDERR.puts "PLEASE REVIEW ERRORS THAT WERE OCCURRED!"
|
312
|
+
end
|
271
313
|
|
272
|
-
|
314
|
+
set_metadata(@options[:test_only] ? "latest_test_only_check_time" : "latest_complete_check_time", time_to_database(Time.now))
|
273
315
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
316
|
+
print_counters
|
317
|
+
end
|
318
|
+
|
319
|
+
puts "Performing database maintenance..." if @options[:verbose]
|
320
|
+
execute "PRAGMA optimize"
|
321
|
+
execute "VACUUM"
|
322
|
+
execute "PRAGMA wal_checkpoint(TRUNCATE)"
|
281
323
|
|
282
|
-
|
324
|
+
hide_database_files
|
325
|
+
end
|
283
326
|
end
|
284
327
|
|
285
328
|
def show_duplicates
|
286
329
|
current_digest = nil
|
287
|
-
|
330
|
+
digests_select_duplicates.each do |found|
|
288
331
|
if current_digest != found["digest"]
|
289
332
|
puts "" if current_digest
|
290
333
|
current_digest = found["digest"]
|
@@ -316,9 +359,10 @@ class FileDigests
|
|
316
359
|
|
317
360
|
normalized_filename = filename.delete_prefix("#{@files_path.to_s}/").encode("utf-8", universal_newline: true).unicode_normalize(:nfkc)
|
318
361
|
mtime_string = time_to_database stat.mtime
|
319
|
-
digest = get_file_digest(filename)
|
362
|
+
digest, new_digest = get_file_digest(filename)
|
320
363
|
|
321
364
|
nested_transaction do
|
365
|
+
new_digests_insert(normalized_filename, new_digest) if new_digest
|
322
366
|
process_file_indeed normalized_filename, mtime_string, digest
|
323
367
|
end
|
324
368
|
|
@@ -336,15 +380,15 @@ class FileDigests
|
|
336
380
|
end
|
337
381
|
|
338
382
|
def process_previously_seen_file found, filename, mtime, digest
|
339
|
-
|
383
|
+
missing_files_delete filename
|
340
384
|
if found["digest"] == digest
|
341
385
|
@counters[:good] += 1
|
342
386
|
puts "GOOD: #{filename}" if @options[:verbose]
|
343
387
|
unless @options[:test_only]
|
344
388
|
if found["mtime"] == mtime
|
345
|
-
|
389
|
+
digests_touch_check_time found["id"]
|
346
390
|
else
|
347
|
-
|
391
|
+
digests_update_mtime mtime, found["id"]
|
348
392
|
end
|
349
393
|
end
|
350
394
|
else
|
@@ -355,18 +399,17 @@ class FileDigests
|
|
355
399
|
@counters[:updated] += 1
|
356
400
|
puts "UPDATED#{" (FATE ACCEPTED)" if found["mtime"] == mtime && @options[:accept_fate]}: #{filename}" unless @options[:quiet]
|
357
401
|
unless @options[:test_only]
|
358
|
-
|
402
|
+
digests_update_mtime_and_digest mtime, digest, found["id"]
|
359
403
|
end
|
360
404
|
end
|
361
405
|
end
|
362
406
|
end
|
363
407
|
|
364
408
|
def process_new_file filename, mtime, digest
|
365
|
-
@counters[:new] += 1
|
366
409
|
puts "NEW: #{filename}" unless @options[:quiet]
|
410
|
+
new_files_insert filename, digest
|
367
411
|
unless @options[:test_only]
|
368
|
-
|
369
|
-
insert filename, mtime, digest
|
412
|
+
digests_insert filename, mtime, digest
|
370
413
|
end
|
371
414
|
end
|
372
415
|
|
@@ -374,29 +417,31 @@ class FileDigests
|
|
374
417
|
# Renames and missing files
|
375
418
|
|
376
419
|
def track_renames
|
377
|
-
@
|
378
|
-
|
379
|
-
@counters[:renamed] += 1
|
380
|
-
unless @options[:test_only]
|
381
|
-
delete_by_filename filename
|
382
|
-
end
|
383
|
-
true
|
384
|
-
end
|
420
|
+
unless @options[:test_only]
|
421
|
+
digests_delete_renamed_files
|
385
422
|
end
|
423
|
+
missing_files_delete_renamed_files
|
424
|
+
@counters[:renamed] = @db.changes
|
386
425
|
end
|
387
426
|
|
388
427
|
def print_missing_files
|
389
428
|
puts "\nMISSING FILES:"
|
390
|
-
|
391
|
-
puts filename
|
429
|
+
missing_files_select_all_filenames.each do |record|
|
430
|
+
puts record["filename"]
|
392
431
|
end
|
393
432
|
end
|
394
433
|
|
395
434
|
def remove_missing_files
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
435
|
+
digests_delete_all_missing_files
|
436
|
+
missing_files_delete_all
|
437
|
+
end
|
438
|
+
|
439
|
+
def missing_files_count
|
440
|
+
missing_files_count_query!&.first&.first
|
441
|
+
end
|
442
|
+
|
443
|
+
def any_missing_files?
|
444
|
+
missing_files_count > 0
|
400
445
|
end
|
401
446
|
|
402
447
|
|
@@ -461,7 +506,7 @@ class FileDigests
|
|
461
506
|
end
|
462
507
|
|
463
508
|
def find_by_filename filename
|
464
|
-
result =
|
509
|
+
result = digests_find_by_filename_query filename
|
465
510
|
found = result.next
|
466
511
|
raise "Multiple records found" if result.next
|
467
512
|
found
|
@@ -481,6 +526,18 @@ class FileDigests
|
|
481
526
|
end
|
482
527
|
end
|
483
528
|
|
529
|
+
def check_if_database_is_at_certain_version target_version
|
530
|
+
current_version = get_metadata("database_version")
|
531
|
+
if current_version != target_version
|
532
|
+
STDERR.puts "This version of file-digests (#{FileDigests::VERSION || "unknown"}) is only compartible with the database version #{target_version}. Current database version is #{current_version}. To use this database, please install appropriate version if file-digest."
|
533
|
+
raise "Incompatible database version"
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
def new_files_count
|
538
|
+
new_files_count_query!&.first&.first
|
539
|
+
end
|
540
|
+
|
484
541
|
|
485
542
|
# Filesystem-related helpers
|
486
543
|
|
@@ -519,18 +576,13 @@ class FileDigests
|
|
519
576
|
digest.update(buffer)
|
520
577
|
new_digest.update(buffer) if @new_digest_algorithm
|
521
578
|
end
|
522
|
-
|
523
|
-
return digest.hexdigest
|
579
|
+
return [digest.hexdigest, (new_digest.hexdigest if @new_digest_algorithm)]
|
524
580
|
end
|
525
581
|
end
|
526
582
|
|
527
583
|
|
528
584
|
# Runtime state helpers
|
529
585
|
|
530
|
-
def any_missing_files?
|
531
|
-
@missing_files.length > 0
|
532
|
-
end
|
533
|
-
|
534
586
|
def any_exceptions?
|
535
587
|
@counters[:exceptions] > 0
|
536
588
|
end
|
@@ -544,14 +596,17 @@ class FileDigests
|
|
544
596
|
def confirm text
|
545
597
|
if STDIN.tty? && STDOUT.tty?
|
546
598
|
puts "#{text} (y/n)?"
|
547
|
-
|
599
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
600
|
+
result = (STDIN.gets.strip.downcase == "y")
|
601
|
+
@user_input_wait_time += (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
|
602
|
+
result
|
548
603
|
end
|
549
604
|
end
|
550
605
|
|
551
606
|
def measure_time
|
552
607
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
553
608
|
yield
|
554
|
-
elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
|
609
|
+
elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) - @user_input_wait_time
|
555
610
|
puts "Elapsed time: #{elapsed.to_i / 3600}h #{(elapsed.to_i % 3600) / 60}m #{"%.3f" % (elapsed % 60)}s" unless @options[:quiet]
|
556
611
|
end
|
557
612
|
|
@@ -569,11 +624,14 @@ class FileDigests
|
|
569
624
|
end
|
570
625
|
|
571
626
|
def print_counters
|
627
|
+
missing_files_count_result = missing_files_count
|
628
|
+
new_files_count_result = new_files_count - @counters[:renamed]
|
629
|
+
|
572
630
|
puts "#{@counters[:good]} file(s) passes digest check" if @counters[:good] > 0
|
573
631
|
puts "#{@counters[:updated]} file(s) are updated" if @counters[:updated] > 0
|
574
|
-
puts "#{
|
632
|
+
puts "#{new_files_count_result} file(s) are new" if new_files_count_result > 0
|
575
633
|
puts "#{@counters[:renamed]} file(s) are renamed" if @counters[:renamed] > 0
|
576
|
-
puts "#{
|
634
|
+
puts "#{missing_files_count_result} file(s) are missing" if missing_files_count_result > 0
|
577
635
|
puts "#{@counters[:likely_damaged]} file(s) are likely damaged (!)" if @counters[:likely_damaged] > 0
|
578
636
|
puts "#{@counters[:exceptions]} file(s) had exceptions occured during processing (!)" if @counters[:exceptions] > 0
|
579
637
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: file-digests
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.34
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stanislav Senotrusov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-10-
|
11
|
+
date: 2020-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: openssl
|