longleaf 0.3.0 → 1.0.0
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/.circleci/config.yml +12 -2
- data/README.md +11 -1
- data/lib/longleaf/candidates/manifest_digest_provider.rb +17 -0
- data/lib/longleaf/candidates/single_digest_provider.rb +13 -0
- data/lib/longleaf/cli.rb +49 -36
- data/lib/longleaf/commands/register_command.rb +3 -3
- data/lib/longleaf/commands/validate_config_command.rb +1 -1
- data/lib/longleaf/events/register_event.rb +8 -4
- data/lib/longleaf/helpers/case_insensitive_hash.rb +38 -0
- data/lib/longleaf/helpers/digest_helper.rb +7 -1
- data/lib/longleaf/helpers/s3_uri_helper.rb +86 -0
- data/lib/longleaf/helpers/selection_options_parser.rb +189 -0
- data/lib/longleaf/helpers/service_date_helper.rb +29 -1
- data/lib/longleaf/indexing/sequel_index_driver.rb +2 -20
- data/lib/longleaf/models/app_fields.rb +4 -2
- data/lib/longleaf/models/filesystem_metadata_location.rb +56 -0
- data/lib/longleaf/models/filesystem_storage_location.rb +52 -0
- data/lib/longleaf/models/metadata_location.rb +47 -0
- data/lib/longleaf/models/metadata_record.rb +3 -1
- data/lib/longleaf/models/s3_storage_location.rb +133 -0
- data/lib/longleaf/models/service_fields.rb +4 -0
- data/lib/longleaf/models/storage_location.rb +17 -48
- data/lib/longleaf/models/storage_types.rb +9 -0
- data/lib/longleaf/preservation_services/rsync_replication_service.rb +9 -11
- data/lib/longleaf/preservation_services/s3_replication_service.rb +143 -0
- data/lib/longleaf/services/application_config_deserializer.rb +26 -4
- data/lib/longleaf/services/application_config_validator.rb +17 -6
- data/lib/longleaf/services/configuration_validator.rb +64 -4
- data/lib/longleaf/services/filesystem_location_validator.rb +16 -0
- data/lib/longleaf/services/metadata_deserializer.rb +41 -9
- data/lib/longleaf/services/metadata_persistence_manager.rb +3 -2
- data/lib/longleaf/services/metadata_serializer.rb +94 -13
- data/lib/longleaf/services/metadata_validator.rb +76 -0
- data/lib/longleaf/services/s3_location_validator.rb +19 -0
- data/lib/longleaf/services/service_definition_validator.rb +16 -8
- data/lib/longleaf/services/service_manager.rb +7 -15
- data/lib/longleaf/services/service_mapping_validator.rb +26 -15
- data/lib/longleaf/services/storage_location_manager.rb +38 -12
- data/lib/longleaf/services/storage_location_validator.rb +41 -30
- data/lib/longleaf/specs/config_builder.rb +10 -3
- data/lib/longleaf/specs/config_validator_helpers.rb +16 -0
- data/lib/longleaf/specs/metadata_builder.rb +1 -0
- data/lib/longleaf/version.rb +1 -1
- data/longleaf.gemspec +3 -1
- data/mkdocs.yml +2 -1
- metadata +48 -8
- data/.travis.yml +0 -4
- data/lib/longleaf/services/storage_path_validator.rb +0 -16
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'longleaf/models/storage_location'
|
2
|
+
require 'longleaf/models/storage_types'
|
3
|
+
require 'longleaf/helpers/s3_uri_helper'
|
4
|
+
require 'uri'
|
5
|
+
require 'aws-sdk-s3'
|
6
|
+
|
7
|
+
module Longleaf
|
8
|
+
# A storage location in a s3 bucket
|
9
|
+
#
|
10
|
+
# Optionally, the location configuration may include an "options" sub-hash in order to provide
|
11
|
+
# any of the s3 client options specified in Client initializer:
|
12
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#constructor_details
|
13
|
+
|
14
|
+
class S3StorageLocation < StorageLocation
|
15
|
+
|
16
|
+
IS_URI_REGEX = /\A#{URI::regexp}\z/
|
17
|
+
|
18
|
+
CLIENT_OPTIONS_FIELD = 'options'
|
19
|
+
|
20
|
+
# @param name [String] the name of this storage location
|
21
|
+
# @param config [Hash] hash containing the configuration options for this location
|
22
|
+
# @param md_loc [MetadataLocation] metadata location associated with this storage location
|
23
|
+
def initialize(name, config, md_loc)
|
24
|
+
super(name, config, md_loc)
|
25
|
+
|
26
|
+
@bucket_name = S3UriHelper.extract_bucket(@path)
|
27
|
+
if @bucket_name.nil?
|
28
|
+
raise ArgumentError.new("Unable to identify bucket for location #{@name} from path #{@path}")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Force path to always end with a slash
|
32
|
+
@path += '/' unless @path.end_with?('/')
|
33
|
+
|
34
|
+
custom_options = config[CLIENT_OPTIONS_FIELD]
|
35
|
+
if custom_options.nil?
|
36
|
+
@client_options = Hash.new
|
37
|
+
else
|
38
|
+
# Clone options and convert keys to symbols
|
39
|
+
@client_options = Hash[custom_options.map { |(k,v)| [k.to_sym,v] } ]
|
40
|
+
end
|
41
|
+
# If no region directly configured, use region from path
|
42
|
+
if !@client_options.key?(:region)
|
43
|
+
region = S3UriHelper.extract_region(@path)
|
44
|
+
@client_options[:region] = region unless region.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
@subpath_prefix = S3UriHelper.extract_path(@path)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return the storage type for this location
|
51
|
+
def type
|
52
|
+
StorageTypes::S3_STORAGE_TYPE
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get that absolute path to the file associated with the provided metadata path
|
56
|
+
# @param md_path [String] metadata file path
|
57
|
+
# @raise [ArgumentError] if the md_path is not in this storage location
|
58
|
+
# @return [String] the path for the file associated with this metadata
|
59
|
+
def get_path_from_metadata_path(md_path)
|
60
|
+
raise ArgumentError.new("A file_path parameter is required") if md_path.nil? || md_path.empty?
|
61
|
+
|
62
|
+
rel_path = @metadata_location.relative_file_path_for(md_path)
|
63
|
+
|
64
|
+
URI.join(@path, rel_path).to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks that the path and metadata path defined in this location are available
|
68
|
+
# @raise [StorageLocationUnavailableError] if the storage location is not available
|
69
|
+
def available?
|
70
|
+
begin
|
71
|
+
s3_client().head_bucket({ bucket: @bucket_name, use_accelerate_endpoint: false })
|
72
|
+
rescue StandardError => e
|
73
|
+
raise StorageLocationUnavailableError.new("Destination bucket #{@bucket_name} does not exist " \
|
74
|
+
+ "or is not accessible: #{e.message}")
|
75
|
+
end
|
76
|
+
@metadata_location.available?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the file path relative to this location
|
80
|
+
# @param file_path [String] file path
|
81
|
+
# @return the file path relative to this location
|
82
|
+
# @raise [ArgumentError] if the file path is not contained by this location
|
83
|
+
def relativize(file_path)
|
84
|
+
raise ArgumentError.new("Must provide a non-nil path to relativize") if file_path.nil?
|
85
|
+
|
86
|
+
if file_path.start_with?(@path)
|
87
|
+
file_path[@path.length..-1]
|
88
|
+
else
|
89
|
+
if file_path =~ IS_URI_REGEX
|
90
|
+
raise ArgumentError.new("Path #{file_path} is not contained by #{@name}")
|
91
|
+
else
|
92
|
+
# path already relative
|
93
|
+
file_path
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Prefixes the provided path with the query path portion of the location's path
|
99
|
+
# after the bucket uri, used to place relative paths into the same sub-URL of a bucket.
|
100
|
+
# For example:
|
101
|
+
# Given a location with 'path' http://example.s3-amazonaws.com/env/test/
|
102
|
+
# Where rel_path = 'path/to/text.txt'
|
103
|
+
# The result would be 'env/test/path/to/text.txt'
|
104
|
+
# @param rel_path relative path to work with
|
105
|
+
# @return the given relative path prefixed with the path portion of the storage location path
|
106
|
+
def relative_to_bucket_path(rel_path)
|
107
|
+
raise ArgumentError.new("Must provide a non-nil path") if rel_path.nil?
|
108
|
+
|
109
|
+
if @subpath_prefix.nil?
|
110
|
+
return rel_path
|
111
|
+
end
|
112
|
+
|
113
|
+
@subpath_prefix + rel_path
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return the bucket used by this storage location
|
117
|
+
def s3_bucket
|
118
|
+
if @bucket.nil?
|
119
|
+
@s3 = Aws::S3::Resource.new(client: s3_client())
|
120
|
+
@bucket = @s3.bucket(@bucket_name)
|
121
|
+
end
|
122
|
+
@bucket
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return the s3 client used by this storage locatio
|
126
|
+
def s3_client
|
127
|
+
if @client.nil?
|
128
|
+
@client = Aws::S3::Client.new(**@client_options)
|
129
|
+
end
|
130
|
+
@client
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -1,34 +1,23 @@
|
|
1
|
-
require 'longleaf/
|
1
|
+
require 'longleaf/models/app_fields'
|
2
2
|
|
3
3
|
module Longleaf
|
4
4
|
# Representation of a configured storage location
|
5
5
|
class StorageLocation
|
6
|
+
AF ||= Longleaf::AppFields
|
7
|
+
|
6
8
|
attr_reader :name
|
7
9
|
attr_reader :path
|
8
|
-
attr_reader :
|
9
|
-
attr_reader :metadata_digests
|
10
|
+
attr_reader :metadata_location
|
10
11
|
|
11
12
|
# @param name [String] the name of this storage location
|
12
|
-
# @param
|
13
|
-
# @param
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@path = path
|
19
|
-
@path += '/' unless @path.end_with?('/')
|
13
|
+
# @param config [Hash] hash containing the configuration options for this location
|
14
|
+
# @param md_loc [MetadataLocation] metadata location associated with this storage location
|
15
|
+
def initialize(name, config, md_loc)
|
16
|
+
raise ArgumentError.new("Config parameter is required") unless config
|
17
|
+
@path = config[AF::LOCATION_PATH]
|
20
18
|
@name = name
|
21
|
-
@
|
22
|
-
@
|
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)
|
19
|
+
raise ArgumentError.new("Parameters name, path and metadata location are required") unless @name && @path && md_loc
|
20
|
+
@metadata_location = md_loc
|
32
21
|
end
|
33
22
|
|
34
23
|
# Get the path for the metadata file for the given file path located in this storage location.
|
@@ -39,35 +28,15 @@ module Longleaf
|
|
39
28
|
raise ArgumentError.new("Provided file path is not contained by storage location #{@name}: #{file_path}") \
|
40
29
|
unless file_path.start_with?(@path)
|
41
30
|
|
42
|
-
|
43
|
-
# If the file_path is to a file, then add metadata suffix.
|
44
|
-
if md_path.end_with?('/')
|
45
|
-
md_path
|
46
|
-
else
|
47
|
-
md_path + MetadataSerializer::metadata_suffix
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Get the metadata path for the provided file path located in this storage location.
|
52
|
-
# @param md_path [String] metadata file path
|
53
|
-
# @raise [ArgumentError] if the md_path is not in this storage location
|
54
|
-
# @return [String] the path for the file associated with this metadata
|
55
|
-
def get_path_from_metadata_path(md_path)
|
56
|
-
raise ArgumentError.new("A file_path parameter is required") if md_path.nil? || md_path.empty?
|
57
|
-
raise ArgumentError.new("Provided metadata path is not contained by storage location #{@name}: #{md_path}") \
|
58
|
-
unless md_path&.start_with?(@metadata_path)
|
31
|
+
rel_file_path = relativize(file_path)
|
59
32
|
|
60
|
-
|
61
|
-
file_path.sub(/#{MetadataSerializer::metadata_suffix}$/, '')
|
33
|
+
@metadata_location.metadata_path_for(rel_file_path)
|
62
34
|
end
|
63
35
|
|
64
|
-
#
|
65
|
-
# @
|
66
|
-
def
|
67
|
-
|
68
|
-
unless Dir.exist?(@path)
|
69
|
-
raise StorageLocationUnavailableError.new("Metadata path does not exist or is not a directory: #{@metadata_path}")\
|
70
|
-
unless Dir.exist?(@metadata_path)
|
36
|
+
# @param [String] path to check
|
37
|
+
# @return true if the file path is contained by the path for this location
|
38
|
+
def contains?(file_path)
|
39
|
+
file_path.start_with?(@path)
|
71
40
|
end
|
72
41
|
end
|
73
42
|
end
|
@@ -4,6 +4,7 @@ require 'longleaf/errors'
|
|
4
4
|
require 'longleaf/models/file_record'
|
5
5
|
require 'longleaf/models/service_fields'
|
6
6
|
require 'longleaf/events/register_event'
|
7
|
+
require 'longleaf/candidates/single_digest_provider'
|
7
8
|
require 'open3'
|
8
9
|
|
9
10
|
module Longleaf
|
@@ -21,10 +22,7 @@ module Longleaf
|
|
21
22
|
# "help", etc. Command will always include "-R". Default "-a".
|
22
23
|
class RsyncReplicationService
|
23
24
|
include Longleaf::Logging
|
24
|
-
|
25
|
-
COLLISION_PROPERTY = "replica_collision_policy"
|
26
|
-
DEFAULT_COLLISION_POLICY = "replace"
|
27
|
-
VALID_COLLISION_POLICIES = ["replace"]
|
25
|
+
SF ||= Longleaf::ServiceFields
|
28
26
|
|
29
27
|
RSYNC_COMMAND_PROPERTY = "rsync_command"
|
30
28
|
DEFAULT_COMMAND = "rsync"
|
@@ -57,14 +55,14 @@ module Longleaf
|
|
57
55
|
@options = @options + " -R"
|
58
56
|
|
59
57
|
# Set and validate the replica collision policy
|
60
|
-
@collision_policy = @service_def.properties[COLLISION_PROPERTY] || DEFAULT_COLLISION_POLICY
|
61
|
-
if !VALID_COLLISION_POLICIES.include?(@collision_policy)
|
62
|
-
raise ArgumentError.new("Service #{service_def.name} received invalid #{COLLISION_PROPERTY}" \
|
63
|
-
+ " value #{collision_policy}")
|
58
|
+
@collision_policy = @service_def.properties[SF::COLLISION_PROPERTY] || SF::DEFAULT_COLLISION_POLICY
|
59
|
+
if !SF::VALID_COLLISION_POLICIES.include?(@collision_policy)
|
60
|
+
raise ArgumentError.new("Service #{service_def.name} received invalid #{SF::COLLISION_PROPERTY}" \
|
61
|
+
+ " value #{@collision_policy}")
|
64
62
|
end
|
65
63
|
|
66
64
|
# Store and validate destinations
|
67
|
-
replicate_to = @service_def.properties[
|
65
|
+
replicate_to = @service_def.properties[SF::REPLICATE_TO]
|
68
66
|
if replicate_to.nil? || replicate_to.empty?
|
69
67
|
raise ArgumentError.new("Service #{service_def.name} must provide one or more replication destinations.")
|
70
68
|
end
|
@@ -105,7 +103,7 @@ module Longleaf
|
|
105
103
|
end
|
106
104
|
|
107
105
|
# Determine the path to the file being replicated relative to its storage location
|
108
|
-
rel_path = file_rec.
|
106
|
+
rel_path = file_rec.storage_location.relativize(file_rec.path)
|
109
107
|
# source path with . so that rsync will only create destination directories starting from that point
|
110
108
|
source_path = File.join(file_rec.storage_location.path, "./#{rel_path}")
|
111
109
|
|
@@ -177,7 +175,7 @@ module Longleaf
|
|
177
175
|
register_event = RegisterEvent.new(file_rec: dest_file_rec,
|
178
176
|
app_manager: @app_manager,
|
179
177
|
force: true,
|
180
|
-
|
178
|
+
digest_provider: SingleDigestProvider.new(file_rec.metadata_record.checksums))
|
181
179
|
register_event.perform
|
182
180
|
end
|
183
181
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'longleaf/events/event_names'
|
2
|
+
require 'longleaf/logging'
|
3
|
+
require 'longleaf/errors'
|
4
|
+
require 'longleaf/models/file_record'
|
5
|
+
require 'longleaf/models/service_fields'
|
6
|
+
require 'longleaf/events/register_event'
|
7
|
+
require 'longleaf/models/storage_types'
|
8
|
+
require 'aws-sdk-s3'
|
9
|
+
|
10
|
+
module Longleaf
|
11
|
+
# Preservation service which performs replication of a file to one or more s3 destinations.
|
12
|
+
#
|
13
|
+
# The service definition must contain one or more destinations, specified with the "to" property.
|
14
|
+
# These destinations must be either a known s3 storage location. The s3 client configuration
|
15
|
+
# is controlled by the storage location.
|
16
|
+
#
|
17
|
+
# Optional service configuration properties:
|
18
|
+
# * replica_collision_policy = specifies the desired outcome if the service attempts to replicate
|
19
|
+
# a file which already exists at a destination. Default: "replace".
|
20
|
+
class S3ReplicationService
|
21
|
+
include Longleaf::Logging
|
22
|
+
ST ||= Longleaf::StorageTypes
|
23
|
+
SF ||= Longleaf::ServiceFields
|
24
|
+
|
25
|
+
attr_reader :collision_policy
|
26
|
+
|
27
|
+
# Initialize a S3ReplicationService from the given service definition
|
28
|
+
#
|
29
|
+
# @param service_def [ServiceDefinition] the configuration for this service
|
30
|
+
# @param app_manager [ApplicationConfigManager] the application configuration
|
31
|
+
def initialize(service_def, app_manager)
|
32
|
+
@service_def = service_def
|
33
|
+
@app_manager = app_manager
|
34
|
+
|
35
|
+
# Set and validate the replica collision policy
|
36
|
+
@collision_policy = @service_def.properties[SF::COLLISION_PROPERTY] || SF::DEFAULT_COLLISION_POLICY
|
37
|
+
if !SF::VALID_COLLISION_POLICIES.include?(@collision_policy)
|
38
|
+
raise ArgumentError.new("Service #{service_def.name} received invalid #{SF::COLLISION_PROPERTY}" \
|
39
|
+
+ " value #{@collision_policy}")
|
40
|
+
end
|
41
|
+
|
42
|
+
# Store and validate destinations
|
43
|
+
replicate_to = @service_def.properties[SF::REPLICATE_TO]
|
44
|
+
if replicate_to.nil? || replicate_to.empty?
|
45
|
+
raise ArgumentError.new("Service #{service_def.name} must provide one or more replication destinations.")
|
46
|
+
end
|
47
|
+
replicate_to = [replicate_to] if replicate_to.is_a?(String)
|
48
|
+
|
49
|
+
loc_manager = app_manager.location_manager
|
50
|
+
# Build list of destinations, translating to storage locations when relevant
|
51
|
+
@destinations = Array.new
|
52
|
+
replicate_to.each do |dest|
|
53
|
+
if loc_manager.locations.key?(dest)
|
54
|
+
location = loc_manager.locations[dest]
|
55
|
+
if location.type != ST::S3_STORAGE_TYPE
|
56
|
+
raise ArgumentError.new(
|
57
|
+
"Service #{service_def.name} specifies destination #{dest} which is not of type 's3'")
|
58
|
+
end
|
59
|
+
@destinations << loc_manager.locations[dest]
|
60
|
+
else
|
61
|
+
raise ArgumentError.new("Service #{service_def.name} specifies unknown storage location '#{dest}'" \
|
62
|
+
+ " as a replication destination")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# During a replication event, perform replication of the specified file to all configured destinations
|
68
|
+
# as necessary.
|
69
|
+
#
|
70
|
+
# @param file_rec [FileRecord] record representing the file to perform the service on.
|
71
|
+
# @param event [String] name of the event this service is being invoked by.
|
72
|
+
# @raise [PreservationServiceError] if the rsync replication fails
|
73
|
+
def perform(file_rec, event)
|
74
|
+
if file_rec.storage_location.type == ST::FILESYSTEM_STORAGE_TYPE
|
75
|
+
replicate_from_fs(file_rec)
|
76
|
+
else
|
77
|
+
raise PreservationServiceError.new("Replication from storage location of type " \
|
78
|
+
+ "#{file_rec.storage_location.type} to s3 is not supported")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def replicate_from_fs(file_rec)
|
83
|
+
# Determine the path to the file being replicated relative to its storage location
|
84
|
+
rel_path = file_rec.storage_location.relativize(file_rec.path)
|
85
|
+
|
86
|
+
content_md5 = get_content_md5(file_rec)
|
87
|
+
|
88
|
+
@destinations.each do |destination|
|
89
|
+
# Check that the destination is available before attempting to write
|
90
|
+
verify_destination_available(destination, file_rec)
|
91
|
+
|
92
|
+
rel_to_bucket = destination.relative_to_bucket_path(rel_path)
|
93
|
+
file_obj = destination.s3_bucket.object(rel_to_bucket)
|
94
|
+
begin
|
95
|
+
file_obj.upload_file(file_rec.path, { :content_md5 => content_md5 })
|
96
|
+
rescue Aws::S3::Errors::BadDigest => e
|
97
|
+
raise ChecksumMismatchError.new("Transfer to bucket '#{destination.s3_bucket.name}' failed, " \
|
98
|
+
+ "MD5 provided did not match the received content for #{file_rec.path}")
|
99
|
+
rescue Aws::Errors::ServiceError => e
|
100
|
+
raise PreservationServiceError.new("Failed to transfer #{file_rec.path} to bucket " \
|
101
|
+
+ "'#{destination.s3_bucket.name}': #{e.message}")
|
102
|
+
end
|
103
|
+
|
104
|
+
logger.info("Replicated #{file_rec.path} to destination #{file_obj.public_url}")
|
105
|
+
|
106
|
+
# TODO register file in destination
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determine if this service is applicable for the provided event, given the configured service definition
|
111
|
+
#
|
112
|
+
# @param event [String] name of the event
|
113
|
+
# @return [Boolean] returns true if this service is applicable for the provided event
|
114
|
+
def is_applicable?(event)
|
115
|
+
case event
|
116
|
+
when EventNames::PRESERVE
|
117
|
+
true
|
118
|
+
else
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def verify_destination_available(destination, file_rec)
|
125
|
+
begin
|
126
|
+
destination.available?
|
127
|
+
rescue StorageLocationUnavailableError => e
|
128
|
+
raise StorageLocationUnavailableError.new("Cannot replicate #{file_rec.path} to destination #{destination.name}: " \
|
129
|
+
+ e.message)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_content_md5(file_rec)
|
134
|
+
md_rec = file_rec.metadata_record
|
135
|
+
if md_rec.checksums.key?('md5')
|
136
|
+
# base 64 encode the digest, as is required by the Content-Md5 header
|
137
|
+
[[md_rec.checksums['md5']].pack("H*")].pack("m0")
|
138
|
+
else
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -19,8 +19,9 @@ module Longleaf
|
|
19
19
|
config_md5 = Digest::MD5.hexdigest(content)
|
20
20
|
|
21
21
|
make_paths_absolute(config_path, config)
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
ApplicationConfigValidator.new(config).validate_config.raise_if_invalid
|
24
|
+
ApplicationConfigManager.new(config, config_md5)
|
24
25
|
end
|
25
26
|
|
26
27
|
def self.load_config_file(config_path)
|
@@ -57,9 +58,30 @@ module Longleaf
|
|
57
58
|
base_pathname = Pathname.new(config_path).expand_path.parent
|
58
59
|
|
59
60
|
config[AF::LOCATIONS].each do |name, properties|
|
60
|
-
properties[AF::LOCATION_PATH] =
|
61
|
+
properties[AF::LOCATION_PATH] = make_file_paths_absolute(base_pathname, properties)
|
61
62
|
|
62
|
-
|
63
|
+
# Resolve single field metadata location into expanded form
|
64
|
+
md_config = properties[AF::METADATA_CONFIG]
|
65
|
+
if md_config.nil?
|
66
|
+
next
|
67
|
+
end
|
68
|
+
if md_config.is_a?(String)
|
69
|
+
md_config = { AF::LOCATION => m_config }
|
70
|
+
end
|
71
|
+
md_config[AF::LOCATION_PATH] = make_file_paths_absolute(base_pathname, md_config)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.make_file_paths_absolute(base_pathname, properties)
|
76
|
+
path = properties[AF::LOCATION_PATH]
|
77
|
+
return nil if path.nil?
|
78
|
+
|
79
|
+
uri = URI(path)
|
80
|
+
|
81
|
+
if uri.scheme.nil? || uri.scheme.casecmp("file") == 0
|
82
|
+
absolution(base_pathname, uri.path)
|
83
|
+
else
|
84
|
+
path
|
63
85
|
end
|
64
86
|
end
|
65
87
|
|