longleaf 0.1.0 → 0.2.0.pre.1
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.
- 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
|