file-digests 0.0.33 → 0.0.34

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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/file-digests +14 -0
  3. data/lib/file-digests.rb +154 -96
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68dea8d33894b138d5908dcf27fef7800a17af8f1441fd043876248414ad9525
4
- data.tar.gz: da5c89c26fc95ddeaa6b8443193293490e71f41bc4ed21e00415c5937661833e
3
+ metadata.gz: dd11bc930ad2146b6e9bbecd44479df1817c8262197a184f19d5db03296d16c1
4
+ data.tar.gz: 60a3741b3b6d7cfa714991bac995176398e4834a53106751b55d537a9e9ac901
5
5
  SHA512:
6
- metadata.gz: 05e700dedeebde8e472e8adf6b0eedb6c3692b919b3d478a041c8e8110375daae7dd926cd9c33c46379976730f0fb0610880a57524aa44e0764fa169a4020fad
7
- data.tar.gz: c0e954b6250223e53ec0823f328bd808886f9566ecf373ceaaba5da7a079ae76e18dfba3cf81bfad0741ea8dfdb87c1715c81f8af72094b77abe014fc59727ef
6
+ metadata.gz: 2cfbe4ac1169e5b48c9bb0c1ee3200fa060e38bc770200d14a127f98ab3967f83476f35130f4bd9d8427bbe7cac07c33b43e14c086e5897921c7383f0748cbfe
7
+ data.tar.gz: 9a5830dc67eb127ff39a0fffda53193d5ad43cfe25405e56d7bd211475af69192f404433c02f2c83d0c3f79dd4a56e4a02f469dbaad52abf6ab2027d8f9894bf
@@ -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
@@ -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
- DIGEST_ALGORITHMS=["BLAKE2b512", "SHA3-256", "SHA512-256"]
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", file_digests_gem_version) if file_digests_gem_version && metadata_table_was_created
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
- set_metadata("digests_table_created_by_gem_version", file_digests_gem_version) if file_digests_gem_version
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 :insert, "INSERT INTO digests (filename, mtime, digest, digest_check_time) VALUES (?, ?, ?, datetime('now'))"
191
- prepare_method :find_by_filename_query, "SELECT id, mtime, digest FROM digests WHERE filename = ?"
192
- prepare_method :touch_digest_check_time, "UPDATE digests SET digest_check_time = datetime('now') WHERE id = ?"
193
- prepare_method :update_mtime_and_digest, "UPDATE digests SET mtime = ?, digest = ?, digest_check_time = datetime('now') WHERE id = ?"
194
- prepare_method :update_mtime, "UPDATE digests SET mtime = ?, digest_check_time = datetime('now') WHERE id = ?"
195
- prepare_method :delete_by_filename, "DELETE FROM digests WHERE filename = ?"
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", "2"
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") != "2"
216
- 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."
217
- raise "Incompatible database version"
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 perform_check
223
- perhaps_transaction(@new_digest_algorithm, :exclusive) do
224
- @counters = {good: 0, updated: 0, new: 0, renamed: 0, likely_damaged: 0, exceptions: 0}
225
- @new_files = {}
226
- @new_digests = {}
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
- @missing_files = Hash[@db.prepare("SELECT filename, digest FROM digests").execute!]
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
- nested_transaction do
237
- puts "Tracking renames..." if @options[:verbose]
238
- track_renames
239
- end
280
+ nested_transaction do
281
+ puts "Tracking renames..." if @options[:verbose]
282
+ track_renames
283
+ end
240
284
 
241
- if any_missing_files?
242
- if any_exceptions?
243
- STDERR.puts "Due to previously occurred errors, database cleanup from missing files will be skipped this time."
244
- else
245
- print_missing_files
246
- if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
247
- nested_transaction do
248
- puts "Removing missing files..." if @options[:verbose]
249
- remove_missing_files
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
- if @new_digest_algorithm && !@options[:test_only]
256
- if any_missing_files? || any_likely_damaged? || any_exceptions?
257
- 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."
258
- else
259
- puts "Updating database to a new digest algorithm..." if @options[:verbose]
260
- @new_digests.each do |old_digest, new_digest|
261
- update_digest_to_new_digest new_digest, old_digest
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
- if any_likely_damaged? || any_exceptions?
269
- STDERR.puts "PLEASE REVIEW ERRORS THAT WERE OCCURRED!"
270
- end
310
+ if any_likely_damaged? || any_exceptions?
311
+ STDERR.puts "PLEASE REVIEW ERRORS THAT WERE OCCURRED!"
312
+ end
271
313
 
272
- set_metadata(@options[:test_only] ? "latest_test_only_check_time" : "latest_complete_check_time", time_to_database(Time.now))
314
+ set_metadata(@options[:test_only] ? "latest_test_only_check_time" : "latest_complete_check_time", time_to_database(Time.now))
273
315
 
274
- print_counters
275
- end
276
-
277
- puts "Performing database maintenance..." if @options[:verbose]
278
- execute "PRAGMA optimize"
279
- execute "VACUUM"
280
- execute "PRAGMA wal_checkpoint(TRUNCATE)"
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
- hide_database_files
324
+ hide_database_files
325
+ end
283
326
  end
284
327
 
285
328
  def show_duplicates
286
329
  current_digest = nil
287
- query_duplicates.each do |found|
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
- @missing_files.delete(filename)
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
- touch_digest_check_time found["id"]
389
+ digests_touch_check_time found["id"]
346
390
  else
347
- update_mtime mtime, found["id"]
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
- update_mtime_and_digest mtime, digest, found["id"]
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
- @new_files[filename] = digest
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
- @missing_files.delete_if do |filename, digest|
378
- if @new_files.value?(digest)
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
- @missing_files.sort.to_h.each do |filename, digest|
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
- @missing_files.each do |filename, digest|
397
- delete_by_filename filename
398
- end
399
- @missing_files = {}
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 = find_by_filename_query filename
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
- @new_digests[digest.hexdigest] = new_digest.hexdigest if @new_digest_algorithm
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
- STDIN.gets.strip.downcase == "y"
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 "#{@counters[:new]} file(s) are new" if @counters[:new] > 0
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 "#{@missing_files.length} file(s) are missing" if @missing_files.length > 0
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.33
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-15 00:00:00.000000000 Z
11
+ date: 2020-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: openssl