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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +13 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +4 -0
  5. data/.rubocop_todo.yml +755 -0
  6. data/README.md +29 -7
  7. data/lib/longleaf/candidates/file_selector.rb +107 -0
  8. data/lib/longleaf/candidates/service_candidate_filesystem_iterator.rb +99 -0
  9. data/lib/longleaf/candidates/service_candidate_locator.rb +18 -0
  10. data/lib/longleaf/cli.rb +102 -6
  11. data/lib/longleaf/commands/deregister_command.rb +50 -0
  12. data/lib/longleaf/commands/preserve_command.rb +45 -0
  13. data/lib/longleaf/commands/register_command.rb +24 -38
  14. data/lib/longleaf/commands/validate_config_command.rb +6 -2
  15. data/lib/longleaf/commands/validate_metadata_command.rb +49 -0
  16. data/lib/longleaf/errors.rb +19 -0
  17. data/lib/longleaf/events/deregister_event.rb +55 -0
  18. data/lib/longleaf/events/event_names.rb +9 -0
  19. data/lib/longleaf/events/event_status_tracking.rb +59 -0
  20. data/lib/longleaf/events/preserve_event.rb +71 -0
  21. data/lib/longleaf/events/register_event.rb +37 -26
  22. data/lib/longleaf/helpers/digest_helper.rb +50 -0
  23. data/lib/longleaf/helpers/service_date_helper.rb +51 -0
  24. data/lib/longleaf/logging.rb +1 -0
  25. data/lib/longleaf/logging/redirecting_logger.rb +9 -8
  26. data/lib/longleaf/models/app_fields.rb +2 -0
  27. data/lib/longleaf/models/file_record.rb +8 -3
  28. data/lib/longleaf/models/md_fields.rb +1 -0
  29. data/lib/longleaf/models/metadata_record.rb +16 -4
  30. data/lib/longleaf/models/service_definition.rb +4 -3
  31. data/lib/longleaf/models/service_fields.rb +2 -0
  32. data/lib/longleaf/models/service_record.rb +4 -1
  33. data/lib/longleaf/models/storage_location.rb +18 -1
  34. data/lib/longleaf/preservation_services/fixity_check_service.rb +121 -0
  35. data/lib/longleaf/preservation_services/rsync_replication_service.rb +183 -0
  36. data/lib/longleaf/services/application_config_deserializer.rb +4 -6
  37. data/lib/longleaf/services/application_config_manager.rb +4 -2
  38. data/lib/longleaf/services/application_config_validator.rb +1 -1
  39. data/lib/longleaf/services/configuration_validator.rb +1 -0
  40. data/lib/longleaf/services/metadata_deserializer.rb +47 -10
  41. data/lib/longleaf/services/metadata_serializer.rb +42 -6
  42. data/lib/longleaf/services/service_class_cache.rb +112 -0
  43. data/lib/longleaf/services/service_definition_manager.rb +5 -1
  44. data/lib/longleaf/services/service_definition_validator.rb +4 -4
  45. data/lib/longleaf/services/service_manager.rb +72 -9
  46. data/lib/longleaf/services/service_mapping_manager.rb +4 -3
  47. data/lib/longleaf/services/service_mapping_validator.rb +4 -4
  48. data/lib/longleaf/services/storage_location_manager.rb +26 -5
  49. data/lib/longleaf/services/storage_location_validator.rb +1 -1
  50. data/lib/longleaf/services/storage_path_validator.rb +2 -2
  51. data/lib/longleaf/specs/config_builder.rb +9 -5
  52. data/lib/longleaf/specs/custom_matchers.rb +9 -0
  53. data/lib/longleaf/specs/file_helpers.rb +60 -0
  54. data/lib/longleaf/version.rb +1 -1
  55. data/longleaf.gemspec +1 -0
  56. metadata +39 -7
  57. 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
@@ -1,6 +1,7 @@
1
1
  require 'longleaf/logging/redirecting_logger'
2
2
 
3
3
  module Longleaf
4
+ # Module for access logging within longleaf
4
5
  module Logging
5
6
  # Get the main logger for longleaf
6
7
  def logger
@@ -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 failure_only [Boolean] If set to true, only failure messages will be output to STDOUT
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 [Strfailure_onlying] format string for log entries to STDERR. There are 4 variables available
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
- # @param eventOrMessage [String] name of the preservation event which succeeded,
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
- # or the message to output if it is the only parameter. Required.
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 [Longleaf::StorageLocation] storage location containing the file
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,4 +1,5 @@
1
1
  module Longleaf
2
+ # File metadata fields
2
3
  class MDFields
3
4
  DATA = 'data'
4
5
  SERVICES = 'services'
@@ -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 :deregistered, :registered
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 service_properties [ServiceRecord] properties for populating the new service
39
- def add_service(name, service = Longleaf::ServiceRecord.new)
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,6 +1,8 @@
1
1
  module Longleaf
2
+ # Constants for common configuration fields for preservation service definitions
2
3
  class ServiceFields
3
4
  WORK_SCRIPT = 'work_script'
5
+ WORK_CLASS = 'work_class'
4
6
  FREQUENCY = 'frequency'
5
7
  DELAY = 'delay'
6
8
 
@@ -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
- def initialize(name:, path:, metadata_path:)
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