Class: FileDigests

Inherits:
Object
  • Object
show all
Defined in:
lib/file-digests.rb

Constant Summary collapse

VERSION =
Gem.loaded_specs["file-digests"]&.version&.to_s
DIGEST_ALGORITHMS =
["BLAKE2b512", "SHA3-256", "SHA512-256"]
LEGACY_DIGEST_ALGORITHMS =
["SHA512", "SHA256"]

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(files_path, digest_database_path, options = {}) ⇒ FileDigests

Returns a new instance of FileDigests.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/file-digests.rb', line 119

def initialize files_path, digest_database_path, options = {}
  @options = options
  @user_input_wait_time = 0

  initialize_paths files_path, digest_database_path
  initialize_database

  @db.transaction(:exclusive) do
    if db_digest_algorithm = ("digest_algorithm")
      if @digest_algorithm = canonical_digest_algorithm_name(db_digest_algorithm)
        if @options[:digest_algorithm] && @options[:digest_algorithm] != @digest_algorithm
          @new_digest_algorithm = @options[:digest_algorithm]
        end
      else
        raise "Database contains data for unsupported digest algorithm: #{db_digest_algorithm}"
      end
    else
      @digest_algorithm = (@options[:digest_algorithm] || "BLAKE2b512")
       "digest_algorithm", @digest_algorithm
    end
  end
  puts "Using #{@digest_algorithm} digest algorithm" if @options[:verbose]
end

Class Method Details

.canonical_digest_algorithm_name(string) ⇒ Object



30
31
32
33
34
35
36
# File 'lib/file-digests.rb', line 30

def self.canonical_digest_algorithm_name(string)
  if string
    algorithms = DIGEST_ALGORITHMS + LEGACY_DIGEST_ALGORITHMS
    index = algorithms.map(&:downcase).index(string.downcase)
    index && algorithms[index]
  end
end

.digest_algorithms_list_textObject



42
43
44
# File 'lib/file-digests.rb', line 42

def self.digest_algorithms_list_text
  "Digest algorithm should be one of the following: #{DIGEST_ALGORITHMS.join ", "}"
end

.parse_cli_optionsObject



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/file-digests.rb', line 46

def self.parse_cli_options
  options = {}

  OptionParser.new do |opts|
    opts.banner = [
      "Usage: file-digests [options] [path/to/directory] [path/to/database_file]",
      "       By default the current directory will be operated upon, and the database file will be placed to the current directory as well.",
      "       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."
    ].join "\n"

    opts.on("-a", "--auto", "Do not ask for any confirmation.") do
      options[:auto] = true
    end

    opts.on(
      "-d", "--digest DIGEST",
      'Select a digest algorithm to use. Default is "BLAKE2b512".',
      'You might also consider to use slower "SHA512-256" or even more slower "SHA3-256".',
      "#{digest_algorithms_list_text}.",
      "You only need to specify an algorithm on the first run, your choice will be saved to a database.",
      "Any time later you could specify a new algorithm to change the current one.",
      "Transition to a new algorithm will only occur if all files pass the check by digests which were stored using the old one."
    ) do |value|
      digest_algorithm = canonical_digest_algorithm_name(value)
      unless DIGEST_ALGORITHMS.include?(digest_algorithm)
        STDERR.puts "ERROR: #{digest_algorithms_list_text}"
        exit 1
      end
      options[:digest_algorithm] = digest_algorithm
    end

    opts.on("-f", "--accept-fate", "Accept the current state of files that are likely damaged and update their digest data.") do
      options[:accept_fate] = true
    end

    opts.on("-h", "--help", "Prints this help.") do
      puts opts
      exit
    end

    opts.on("-p", "--duplicates", "Show the list of duplicate files, based on the information out of the database.") do
      options[:action] = :show_duplicates
    end

    opts.on("-q", "--quiet", "Less verbose output, stil report any found issues.") do
      options[:quiet] = true
    end

    opts.on(
      "-t", "--test",
      "Perform a test to verify directory contents.",
      "Compare actual files with the stored digests, check if any files are missing.",
      "Digest database will not be modified."
    ) do
      options[:test_only] = true
    end

    opts.on("-v", "--verbose", "More verbose output.") do
      options[:verbose] = true
    end

  end.parse!
  options
end

.run_cli_utilityObject



111
112
113
114
115
116
117
# File 'lib/file-digests.rb', line 111

def self.run_cli_utility
  options = parse_cli_options

  file_digests = self.new ARGV[0], ARGV[1], options
  file_digests.send(options[:action] || :perform_check)
  file_digests.close_database
end

Instance Method Details

#canonical_digest_algorithm_name(string) ⇒ Object



38
39
40
# File 'lib/file-digests.rb', line 38

def canonical_digest_algorithm_name string
  self.class.canonical_digest_algorithm_name string
end

#close_databaseObject



213
214
215
216
217
# File 'lib/file-digests.rb', line 213

def close_database
  @statements.each(&:close)
  @db.close
  hide_database_files
end

#perform_checkObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/file-digests.rb', line 143

def perform_check
  measure_time do
    perhaps_transaction(@new_digest_algorithm, :exclusive) do
      @counters = {good: 0, updated: 0, renamed: 0, likely_damaged: 0, exceptions: 0}

      walk_files(@files_path) do |filename|
        process_file filename
      end

      nested_transaction do
        puts "Tracking renames..." if @options[:verbose]
        track_renames
      end

      if any_missing_files?
        if any_exceptions?
          STDERR.puts "Due to previously occurred errors, missing files will not removed from the database."
        else
          report_missing_files
          if !@options[:test_only] && (@options[:auto] || confirm("Remove missing files from the database"))
            nested_transaction do
              puts "Removing missing files..." if @options[:verbose]
              remove_missing_files
            end
          end
        end
      end

      if @new_digest_algorithm && !@options[:test_only]
        if any_missing_files? || any_likely_damaged? || any_exceptions?
          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."
        else
          puts "Updating database to a new digest algorithm..." if @options[:verbose]
          digests_update_digests_to_new_digests
           "digest_algorithm", @new_digest_algorithm
          puts "Transition to a new digest algorithm complete: #{@new_digest_algorithm}"
        end
      end

      if any_likely_damaged? || any_exceptions?
        STDERR.puts "PLEASE REVIEW ERRORS THAT WERE OCCURRED!"
        STDERR.puts "A list of errors is also saved in a file: #{@error_log_path}"
      end

      print_counters

      if any_missing_files? || any_likely_damaged? || any_exceptions?
        $FILE_DIGESTS_EXIT_STATUS=1
      end
    end

    puts "Performing database maintenance..." if @options[:verbose]
    execute "PRAGMA optimize"
    execute "VACUUM"
    execute "PRAGMA wal_checkpoint(TRUNCATE)"
  end
end

#show_duplicatesObject



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/file-digests.rb', line 201

def show_duplicates
  current_digest = nil
  digests_select_duplicates.each do |found|
    if current_digest != found["digest"]
      puts "" if current_digest
      current_digest = found["digest"]
      puts "#{found["digest"]}:"
    end
    puts "  #{found["filename"]}"
  end
end