longleaf 0.1.0 → 0.2.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +13 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +755 -0
- data/README.md +29 -7
- data/lib/longleaf/candidates/file_selector.rb +107 -0
- data/lib/longleaf/candidates/service_candidate_filesystem_iterator.rb +99 -0
- data/lib/longleaf/candidates/service_candidate_locator.rb +18 -0
- data/lib/longleaf/cli.rb +102 -6
- data/lib/longleaf/commands/deregister_command.rb +50 -0
- data/lib/longleaf/commands/preserve_command.rb +45 -0
- data/lib/longleaf/commands/register_command.rb +24 -38
- data/lib/longleaf/commands/validate_config_command.rb +6 -2
- data/lib/longleaf/commands/validate_metadata_command.rb +49 -0
- data/lib/longleaf/errors.rb +19 -0
- data/lib/longleaf/events/deregister_event.rb +55 -0
- data/lib/longleaf/events/event_names.rb +9 -0
- data/lib/longleaf/events/event_status_tracking.rb +59 -0
- data/lib/longleaf/events/preserve_event.rb +71 -0
- data/lib/longleaf/events/register_event.rb +37 -26
- data/lib/longleaf/helpers/digest_helper.rb +50 -0
- data/lib/longleaf/helpers/service_date_helper.rb +51 -0
- data/lib/longleaf/logging.rb +1 -0
- data/lib/longleaf/logging/redirecting_logger.rb +9 -8
- data/lib/longleaf/models/app_fields.rb +2 -0
- data/lib/longleaf/models/file_record.rb +8 -3
- data/lib/longleaf/models/md_fields.rb +1 -0
- data/lib/longleaf/models/metadata_record.rb +16 -4
- data/lib/longleaf/models/service_definition.rb +4 -3
- data/lib/longleaf/models/service_fields.rb +2 -0
- data/lib/longleaf/models/service_record.rb +4 -1
- data/lib/longleaf/models/storage_location.rb +18 -1
- data/lib/longleaf/preservation_services/fixity_check_service.rb +121 -0
- data/lib/longleaf/preservation_services/rsync_replication_service.rb +183 -0
- data/lib/longleaf/services/application_config_deserializer.rb +4 -6
- data/lib/longleaf/services/application_config_manager.rb +4 -2
- data/lib/longleaf/services/application_config_validator.rb +1 -1
- data/lib/longleaf/services/configuration_validator.rb +1 -0
- data/lib/longleaf/services/metadata_deserializer.rb +47 -10
- data/lib/longleaf/services/metadata_serializer.rb +42 -6
- data/lib/longleaf/services/service_class_cache.rb +112 -0
- data/lib/longleaf/services/service_definition_manager.rb +5 -1
- data/lib/longleaf/services/service_definition_validator.rb +4 -4
- data/lib/longleaf/services/service_manager.rb +72 -9
- data/lib/longleaf/services/service_mapping_manager.rb +4 -3
- data/lib/longleaf/services/service_mapping_validator.rb +4 -4
- data/lib/longleaf/services/storage_location_manager.rb +26 -5
- data/lib/longleaf/services/storage_location_validator.rb +1 -1
- data/lib/longleaf/services/storage_path_validator.rb +2 -2
- data/lib/longleaf/specs/config_builder.rb +9 -5
- data/lib/longleaf/specs/custom_matchers.rb +9 -0
- data/lib/longleaf/specs/file_helpers.rb +60 -0
- data/lib/longleaf/version.rb +1 -1
- data/longleaf.gemspec +1 -0
- metadata +39 -7
- data/lib/longleaf/commands/abstract_command.rb +0 -37
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'longleaf/errors'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
module Longleaf
|
5
|
+
# Helper methods for generating digests
|
6
|
+
class DigestHelper
|
7
|
+
KNOWN_DIGESTS ||= ['md5', 'sha1', 'sha2', 'sha256', 'sha384', 'sha512', 'rmd160']
|
8
|
+
|
9
|
+
# @param algs Either a string containing one or an array containing zero or more digest
|
10
|
+
# algorithm names.
|
11
|
+
# @raise [InvalidDigestAlgorithmError] thrown if any of the digest algorithms listed are not
|
12
|
+
# known to the system.
|
13
|
+
def self.validate_algorithms(algs)
|
14
|
+
return if algs.nil?
|
15
|
+
if algs.is_a?(String)
|
16
|
+
unless KNOWN_DIGESTS.include?(algs)
|
17
|
+
raise InvalidDigestAlgorithmError.new("Unknown digest algorithm #{algs}")
|
18
|
+
end
|
19
|
+
else
|
20
|
+
unknown = algs.select { |alg| !KNOWN_DIGESTS.include?(alg) }
|
21
|
+
unless unknown.empty?
|
22
|
+
raise InvalidDigestAlgorithmError.new("Unknown digest algorithm(s): #{unknown.to_s}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get a Digest class for the specified algorithm
|
28
|
+
# @param alg [String] name of the digest algorithm
|
29
|
+
# @return [Digest] A digest class for the requested algorithm
|
30
|
+
# @raise [InvalidDigestAlgorithmError] if an unknown digest algorithm is requested
|
31
|
+
def self.start_digest(alg)
|
32
|
+
case alg
|
33
|
+
when 'md5'
|
34
|
+
return Digest::MD5.new
|
35
|
+
when 'sha1'
|
36
|
+
return Digest::SHA1.new
|
37
|
+
when 'sha2', 'sha256'
|
38
|
+
return Digest::SHA2.new
|
39
|
+
when 'sha384'
|
40
|
+
return Digest::SHA2.new(384)
|
41
|
+
when 'sha512'
|
42
|
+
return Digest::SHA2.new(512)
|
43
|
+
when 'rmd160'
|
44
|
+
return Digest::RMD160.new
|
45
|
+
else
|
46
|
+
raise InvalidDigestAlgorithmError.new("Cannot produce digest for unknown algorithm '#{alg}'.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Longleaf
|
4
|
+
# Helper methods for interacting with dates/timestamps on services
|
5
|
+
class ServiceDateHelper
|
6
|
+
|
7
|
+
# Adds the amount of time from modifier to the provided timestamp
|
8
|
+
# @param timestamp [String] ISO-8601 timestamp string
|
9
|
+
# @param modifier [String] amount of time to add to the timestamp. It must follow the syntax
|
10
|
+
# "<quantity> <time unit>", where quantity must be a positive whole number and time unit
|
11
|
+
# must be second, minute, hour, day, week, month or year (unit may be plural).
|
12
|
+
# Any info after a comma will be ignored.
|
13
|
+
# @return [String] the original timestamp in ISO-8601 format with the provided amount of time added.
|
14
|
+
def self.add_to_timestamp(timestamp, modifier)
|
15
|
+
if modifier =~ /^(\d+) *(second|minute|hour|day|week|month|year)s?(,.*)?/
|
16
|
+
value = $1.to_i
|
17
|
+
unit = $2
|
18
|
+
else
|
19
|
+
raise ArgumentError.new("Cannot parse time modifier #{modifier}")
|
20
|
+
end
|
21
|
+
|
22
|
+
datetime = Time.iso8601(timestamp)
|
23
|
+
case unit
|
24
|
+
when 'second'
|
25
|
+
unit_modifier = 1
|
26
|
+
when 'minute'
|
27
|
+
unit_modifier = 60
|
28
|
+
when 'hour'
|
29
|
+
unit_modifier = 3600
|
30
|
+
when 'day'
|
31
|
+
unit_modifier = 24 * 3600
|
32
|
+
when 'week'
|
33
|
+
unit_modifier = 7 * 24 * 3600
|
34
|
+
when 'month'
|
35
|
+
unit_modifier = 30 * 24 * 3600
|
36
|
+
when 'year'
|
37
|
+
unit_modifier = 365 * 24 * 3600
|
38
|
+
end
|
39
|
+
|
40
|
+
modified_time = datetime + (value * unit_modifier)
|
41
|
+
modified_time.iso8601
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get a timestamp in the format expected for service timestamps.
|
45
|
+
# @param timestamp [Time] the time to format. Defaults to now.
|
46
|
+
# @return [String] the time formatted as iso8601
|
47
|
+
def self.formatted_timestamp(timestamp = Time.now)
|
48
|
+
timestamp.iso8601.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/longleaf/logging.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
require 'logger'
|
2
2
|
|
3
|
-
# Logger which directs messages to stdout and/or stderr, depending on the nature of the message.
|
4
|
-
# Status logging, which includes standard logger methods, goes to STDERR.
|
5
|
-
# Operation success and failure messages go to STDOUT, and to STDERR at info level.
|
6
3
|
module Longleaf
|
7
4
|
module Logging
|
5
|
+
# Logger which directs messages to stdout and/or stderr, depending on the nature of the message.
|
6
|
+
# Status logging, which includes standard logger methods, goes to STDERR.
|
7
|
+
# Operation success and failure messages go to STDOUT, and to STDERR at info level.
|
8
8
|
class RedirectingLogger
|
9
|
-
# @param
|
9
|
+
# @param [Boolean] failure_only If set to true, only failure messages will be output to STDOUT
|
10
10
|
# @param log_level [String] logger level used for output to STDERR
|
11
|
-
# @param log_format [
|
11
|
+
# @param log_format [String] format string for log entries to STDERR. There are 4 variables available
|
12
12
|
# for inclusion in the output: severity, datetime, progname, msg. Variables must be wrapped in %{}.
|
13
13
|
# @param datetime_format [String] datetime formatting string used for logger dates appearing in STDERR.
|
14
14
|
def initialize(failure_only: false, log_level: 'WARN', log_format: nil, datetime_format: nil)
|
@@ -67,7 +67,8 @@ module Longleaf
|
|
67
67
|
end
|
68
68
|
|
69
69
|
# Logs a success message to STDOUT, as well as STDERR at info level.
|
70
|
-
#
|
70
|
+
#
|
71
|
+
# @param [String] eventOrMessage name of the preservation event which succeeded,
|
71
72
|
# or the message to output if it is the only parameter. Required.
|
72
73
|
# @param file_name [String] file name which is the subject of this message.
|
73
74
|
# @param message [String] descriptive message to accompany this output
|
@@ -90,6 +91,7 @@ module Longleaf
|
|
90
91
|
|
91
92
|
@stderr_log.info(text)
|
92
93
|
@stderr_log.error("#{error.message}") unless error.nil?
|
94
|
+
@stderr_log.error("#{error.backtrace.to_s}") unless error.nil? || error.backtrace.nil?
|
93
95
|
end
|
94
96
|
|
95
97
|
# Logs an outcome message to STDOUT, as well as STDERR at info level.
|
@@ -97,7 +99,7 @@ module Longleaf
|
|
97
99
|
#
|
98
100
|
# @param outcome [String] The status of the outcome. Required.
|
99
101
|
# @param eventOrMessage [String] name of the preservation event which was successful,
|
100
|
-
#
|
102
|
+
# or the message to output if it is the only parameter. Required.
|
101
103
|
# @param file_name [String] file name which is the subject of this message.
|
102
104
|
# @param message [String] descriptive message to accompany this output
|
103
105
|
# @param service [String] name of the service which executed.
|
@@ -108,7 +110,6 @@ module Longleaf
|
|
108
110
|
@stderr_log.info(text)
|
109
111
|
end
|
110
112
|
|
111
|
-
# FAILURE verify[cdr_fixity_check] /path/to/file: Something terrible
|
112
113
|
private
|
113
114
|
def outcome_text(outcome, eventOrMessage, file_name = nil, message = nil, service = nil, error = nil)
|
114
115
|
message_only = file_name.nil? && message.nil? && error.nil?
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module Longleaf
|
2
|
+
# Application configuration field names
|
2
3
|
class AppFields
|
3
4
|
LOCATIONS = 'locations'
|
4
5
|
SERVICES = 'services'
|
@@ -6,5 +7,6 @@ module Longleaf
|
|
6
7
|
|
7
8
|
LOCATION_PATH = 'path'
|
8
9
|
METADATA_PATH = 'metadata_path'
|
10
|
+
METADATA_DIGESTS = 'metadata_digests'
|
9
11
|
end
|
10
12
|
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
# Record for an individual file and its associated information
|
2
1
|
module Longleaf
|
2
|
+
# Record for an individual file and its associated information
|
3
3
|
class FileRecord
|
4
4
|
|
5
5
|
attr_accessor :metadata_record
|
@@ -7,13 +7,14 @@ module Longleaf
|
|
7
7
|
attr_reader :path
|
8
8
|
|
9
9
|
# @param file_path [String] path to the file
|
10
|
-
# @param storage_location [
|
11
|
-
def initialize(file_path, storage_location)
|
10
|
+
# @param storage_location [StorageLocation] storage location containing the file
|
11
|
+
def initialize(file_path, storage_location, metadata_record = nil)
|
12
12
|
raise ArgumentError.new("FileRecord requires a path") if file_path.nil?
|
13
13
|
raise ArgumentError.new("FileRecord requires a storage_location") if storage_location.nil?
|
14
14
|
|
15
15
|
@path = file_path
|
16
16
|
@storage_location = storage_location
|
17
|
+
@metadata_record = metadata_record
|
17
18
|
end
|
18
19
|
|
19
20
|
# @return [String] path for the metadata file for this file
|
@@ -21,5 +22,9 @@ module Longleaf
|
|
21
22
|
@metadata_path = @storage_location.get_metadata_path_for(path) if @metadata_path.nil?
|
22
23
|
@metadata_path
|
23
24
|
end
|
25
|
+
|
26
|
+
def metadata_present?
|
27
|
+
File.exist?(metadata_path)
|
28
|
+
end
|
24
29
|
end
|
25
30
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
require_relative 'md_fields'
|
2
2
|
require_relative 'service_record'
|
3
3
|
|
4
|
-
# Metadata record for a single file
|
5
4
|
module Longleaf
|
5
|
+
# Metadata record for a single file
|
6
6
|
class MetadataRecord
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :registered
|
8
|
+
attr_accessor :deregistered
|
8
9
|
attr_reader :checksums
|
9
10
|
attr_reader :properties
|
10
11
|
attr_accessor :file_size, :last_modified
|
@@ -35,14 +36,25 @@ module Longleaf
|
|
35
36
|
# Adds a service to this record
|
36
37
|
#
|
37
38
|
# @param name [String] identifier for the service being added
|
38
|
-
# @param
|
39
|
-
|
39
|
+
# @param service [ServiceRecord] properties for populating the new service
|
40
|
+
# @return [ServiceRecord] the service added
|
41
|
+
def add_service(name, service = ServiceRecord.new)
|
40
42
|
raise ArgumentError.new("Value must be a ServiceRecord object when adding a service") unless service.class == Longleaf::ServiceRecord
|
41
43
|
raise IndexError.new("Service with name '#{name}' already exists") if @services.key?(name)
|
42
44
|
|
43
45
|
@services[name] = service
|
44
46
|
end
|
45
47
|
|
48
|
+
# Updates details of service record as if the service had been executed.
|
49
|
+
# @param service_name [String] name of the service run
|
50
|
+
# @return [ServiceRecord] the service record updated
|
51
|
+
def update_service_as_performed(service_name)
|
52
|
+
service_rec = service(service_name) || add_service(service_name)
|
53
|
+
service_rec.run_needed = false
|
54
|
+
service_rec.timestamp = ServiceDateHelper.formatted_timestamp
|
55
|
+
service_rec
|
56
|
+
end
|
57
|
+
|
46
58
|
# @param name [String] name identifier of the service to retrieve
|
47
59
|
# @return [ServiceRecord] the ServiceRecord for the service identified by name, or nil
|
48
60
|
def service(name)
|
@@ -1,19 +1,20 @@
|
|
1
1
|
require_relative 'service_fields'
|
2
2
|
|
3
|
-
# Definition of a preservation service
|
4
3
|
module Longleaf
|
4
|
+
# Definition of a configured preservation service
|
5
5
|
class ServiceDefinition
|
6
6
|
attr_reader :name
|
7
|
-
attr_reader :work_script
|
7
|
+
attr_reader :work_script, :work_class
|
8
8
|
attr_reader :frequency, :delay
|
9
9
|
attr_reader :properties
|
10
10
|
|
11
|
-
def initialize(name:, work_script:, frequency: nil, delay: nil, properties: Hash.new)
|
11
|
+
def initialize(name:, work_script:, work_class: nil, frequency: nil, delay: nil, properties: Hash.new)
|
12
12
|
raise ArgumentError.new("Parameters name and work_script are required") unless name && work_script
|
13
13
|
|
14
14
|
@properties = properties
|
15
15
|
@name = name
|
16
16
|
@work_script = work_script
|
17
|
+
@work_class = work_class
|
17
18
|
@frequency = frequency
|
18
19
|
@delay = delay
|
19
20
|
end
|
@@ -1,10 +1,13 @@
|
|
1
|
-
# Record for an individual service in a file's metadata record.
|
2
1
|
module Longleaf
|
2
|
+
# Record for an individual service in a file's metadata record.
|
3
3
|
class ServiceRecord
|
4
4
|
attr_reader :properties
|
5
5
|
attr_accessor :stale_replicas, :timestamp, :run_needed
|
6
6
|
|
7
7
|
# @param properties [Hash] initial properties for this service record
|
8
|
+
# @param stale_replicas [Boolean] whether there are any stale replicas from this service
|
9
|
+
# @param timestamp [String] timestamp when this service last ran or was initialized
|
10
|
+
# @param run_needed [Boolean] flag indicating that this service should be run at the next available opportunity
|
8
11
|
def initialize(properties: Hash.new, stale_replicas: false, timestamp: nil, run_needed: false)
|
9
12
|
raise ArgumentError.new("Service properties must be a hash") if properties.class != Hash
|
10
13
|
|
@@ -1,17 +1,34 @@
|
|
1
1
|
require 'longleaf/services/metadata_serializer'
|
2
2
|
|
3
3
|
module Longleaf
|
4
|
+
# Representation of a configured storage location
|
4
5
|
class StorageLocation
|
5
6
|
attr_reader :name
|
6
7
|
attr_reader :path
|
7
8
|
attr_reader :metadata_path
|
9
|
+
attr_reader :metadata_digests
|
8
10
|
|
9
|
-
|
11
|
+
# @param name [String] the name of this storage location
|
12
|
+
# @param path [String] absolute path where the storage location is located
|
13
|
+
# @param metadata_path [String] absolute path where the metadata for files in this location will be stored.
|
14
|
+
# @param metadata_digests list of digest algorithms to use for metadata file digests in this location.
|
15
|
+
def initialize(name:, path:, metadata_path:, metadata_digests: [])
|
10
16
|
raise ArgumentError.new("Parameters name, path and metadata_path are required") unless name && path && metadata_path
|
11
17
|
|
12
18
|
@path = path
|
19
|
+
@path += '/' unless @path.end_with?('/')
|
13
20
|
@name = name
|
14
21
|
@metadata_path = metadata_path
|
22
|
+
@metadata_path += '/' unless @metadata_path.end_with?('/')
|
23
|
+
|
24
|
+
if metadata_digests.nil?
|
25
|
+
@metadata_digests = []
|
26
|
+
elsif metadata_digests.is_a?(String)
|
27
|
+
@metadata_digests = [metadata_digests.downcase]
|
28
|
+
else
|
29
|
+
@metadata_digests = metadata_digests.map(&:downcase)
|
30
|
+
end
|
31
|
+
DigestHelper::validate_algorithms(@metadata_digests)
|
15
32
|
end
|
16
33
|
|
17
34
|
# Get the path for the metadata file for the given file path located in this storage location.
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'longleaf/events/event_names'
|
2
|
+
require 'longleaf/models/service_fields'
|
3
|
+
require 'longleaf/logging'
|
4
|
+
require 'longleaf/helpers/digest_helper'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module Longleaf
|
8
|
+
# Preservation service which performs one or more fixity checks on a file based on the configured list
|
9
|
+
# of digest algorithms. It currently supports 'md5', 'sha1', 'sha2', 'sha256', 'sha384', 'sha512' and 'rmd160'.
|
10
|
+
#
|
11
|
+
# If the service encounters a file which is missing any of the digest algorithms the service is configured
|
12
|
+
# to check, the outcome may be controlled with the 'absent_digest' property via the following values:
|
13
|
+
# * 'fail' - the service will raise a ChecksumMismatchError for the missing algorithm. This is the default.
|
14
|
+
# * 'ignore' - the service will skip calculating any algorithms not already present for the file.
|
15
|
+
# * 'generate' - the service will generate and store any missing digests from the set of configured algorithms.
|
16
|
+
class FixityCheckService
|
17
|
+
include Longleaf::Logging
|
18
|
+
|
19
|
+
SUPPORTED_ALGORITHMS = ['md5', 'sha1', 'sha2', 'sha256', 'sha384', 'sha512', 'rmd160']
|
20
|
+
|
21
|
+
# service configuration property indicating how to handle situations where a file does not
|
22
|
+
# have a digest for one of the expected algorithms on record.
|
23
|
+
ABSENT_DIGEST_PROPERTY = 'absent_digest'
|
24
|
+
FAIL_IF_ABSENT = 'fail'
|
25
|
+
GENERATE_IF_ABSENT = 'generate'
|
26
|
+
IGNORE_IF_ABSENT = 'ignore'
|
27
|
+
ABSENT_DIGEST_OPTIONS = [FAIL_IF_ABSENT, GENERATE_IF_ABSENT, IGNORE_IF_ABSENT]
|
28
|
+
|
29
|
+
# Initialize a FixityCheckService from the given service definition
|
30
|
+
#
|
31
|
+
# @param service_def [ServiceDefinition] the configuration for this service
|
32
|
+
# @param app_manager [ApplicationConfigManager] manager for configured storage locations
|
33
|
+
def initialize(service_def, app_manager)
|
34
|
+
@service_def = service_def
|
35
|
+
@absent_digest_behavior = @service_def.properties[ABSENT_DIGEST_PROPERTY] || FAIL_IF_ABSENT
|
36
|
+
unless ABSENT_DIGEST_OPTIONS.include?(@absent_digest_behavior)
|
37
|
+
raise ArgumentError.new("Invalid option '#{@absent_digest_behavior}' for property #{ABSENT_DIGEST_PROPERTY} in service #{service_def.name}")
|
38
|
+
end
|
39
|
+
|
40
|
+
service_algs = service_def.properties[ServiceFields::DIGEST_ALGORITHMS]
|
41
|
+
if service_algs.nil? || service_algs.empty?
|
42
|
+
raise ArgumentError.new("FixityCheckService from definition #{service_def.name} requires a list of one or more digest algorithms")
|
43
|
+
end
|
44
|
+
|
45
|
+
# Store the list of digest algorithms to verify, using normalized algorithm names.
|
46
|
+
@digest_algs = Set.new
|
47
|
+
service_algs.each do |alg|
|
48
|
+
normalized_alg = alg.downcase.delete('-')
|
49
|
+
if SUPPORTED_ALGORITHMS.include?(normalized_alg)
|
50
|
+
@digest_algs << normalized_alg
|
51
|
+
else
|
52
|
+
raise ArgumentError.new("Unsupported checksum algorithm '#{alg}' in definition #{service_def.name}. Supported algorithms are: #{SUPPORTED_ALGORITHMS.to_s}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Perform all configured fixity checks on the provided file
|
58
|
+
#
|
59
|
+
# @param file_rec [FileRecord] record representing the file to perform the service on.
|
60
|
+
# @param event [String] name of the event this service is being invoked by.
|
61
|
+
# @raise [ChecksumMismatchError] if the checksum on record does not match the generated checksum
|
62
|
+
def perform(file_rec, event)
|
63
|
+
path = file_rec.path
|
64
|
+
md_rec = file_rec.metadata_record
|
65
|
+
|
66
|
+
# Get the list of existing checksums for the file and normalize algorithm names
|
67
|
+
file_digests = Hash.new
|
68
|
+
md_rec.checksums&.each do |alg, digest|
|
69
|
+
normalized_alg = alg.downcase.delete('-')
|
70
|
+
if @digest_algs.include?(normalized_alg)
|
71
|
+
file_digests[normalized_alg] = digest
|
72
|
+
else
|
73
|
+
logger.debug("Metadata for file #{path} contains unexpected '#{alg}' digest, it will be ignored.")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@digest_algs.each do |alg|
|
78
|
+
existing_digest = file_digests[alg]
|
79
|
+
|
80
|
+
if existing_digest.nil?
|
81
|
+
if @absent_digest_behavior == FAIL_IF_ABSENT
|
82
|
+
raise ChecksumMismatchError.new("Fixity check using algorithm '#{alg}' failed for file #{path}: no existing digest of type '#{alg}' on record.")
|
83
|
+
elsif @absent_digest_behavior == IGNORE_IF_ABSENT
|
84
|
+
logger.debug("Skipping check of algorithm '#{alg}' for file #{path}: no digest on record.")
|
85
|
+
next
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
digest = DigestHelper::start_digest(alg)
|
90
|
+
digest.file(path)
|
91
|
+
generated_digest = digest.hexdigest
|
92
|
+
|
93
|
+
# Store the missing checksum if using the 'generate' behavior
|
94
|
+
if existing_digest.nil? && @absent_digest_behavior == GENERATE_IF_ABSENT
|
95
|
+
md_rec.checksums[alg] = generated_digest
|
96
|
+
logger.info("Generated and stored digest using algorithm '#{alg}' for file #{path}")
|
97
|
+
else
|
98
|
+
# Compare the new digest to the one on record
|
99
|
+
if existing_digest == generated_digest
|
100
|
+
logger.info("Fixity check using algorithm '#{alg}' succeeded for file #{path}")
|
101
|
+
else
|
102
|
+
raise ChecksumMismatchError.new("Fixity check using algorithm '#{alg}' failed for file #{path}: expected '#{existing_digest}', calculated '#{generated_digest}.'")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Determine if this service is applicable for the provided event, given the configured service definition
|
109
|
+
#
|
110
|
+
# @param event [String] name of the event
|
111
|
+
# @return [Boolean] returns true if this service is applicable for the provided event
|
112
|
+
def is_applicable?(event)
|
113
|
+
case event
|
114
|
+
when EventNames::PRESERVE
|
115
|
+
true
|
116
|
+
else
|
117
|
+
false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|