moab-versioning 5.2.0 → 6.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/moab/bagger.rb +6 -6
- data/lib/moab/file_group.rb +4 -2
- data/lib/moab/file_group_difference.rb +4 -4
- data/lib/moab/file_inventory.rb +8 -6
- data/lib/moab/file_inventory_difference.rb +7 -7
- data/lib/moab/file_signature.rb +4 -4
- data/lib/moab/storage_object.rb +2 -2
- data/lib/moab/storage_object_validator.rb +23 -40
- data/lib/moab/storage_object_version.rb +8 -8
- data/lib/moab/storage_repository.rb +6 -32
- data/lib/moab/storage_services.rb +25 -23
- data/lib/moab/utc_time.rb +3 -3
- data/lib/moab/verification_result.rb +1 -1
- data/lib/serializer/manifest.rb +3 -3
- data/lib/serializer/serializable.rb +1 -1
- data/lib/serializer.rb +0 -1
- data/lib/stanford/content_inventory.rb +14 -14
- data/lib/stanford/storage_services.rb +15 -2
- metadata +21 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d208d4a3118a616d2ee2580e51d9a344438676755f5ef33ace82ed2dbc33bba
|
4
|
+
data.tar.gz: 7198c3f7450d8a6bb61d8b33d50404e9b0a07e52d47198f713b4400e1c2a4435
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 884f1c7e2818e9bcb64de45939cda1ccc4a1aebf5015edd3bfefe707f3a4185253aaa629d4cc0340145be5fd2a72aac0931c7115e8a7929338dd362ff9aa894b
|
7
|
+
data.tar.gz: 2f23fc485263350eb667eed7871dc194972688151613d573227dfa3b2f5490d9f6ccc8f7625f0c4ac3fa66ac0ee01f3c470a24b6133443c89f080e90bae273aa
|
data/lib/moab/bagger.rb
CHANGED
@@ -58,9 +58,9 @@ module Moab
|
|
58
58
|
# @return [void] Generate the bagit.txt tag file
|
59
59
|
def create_bagit_txt
|
60
60
|
bag_pathname.mkpath
|
61
|
-
bag_pathname.join(
|
62
|
-
f.puts
|
63
|
-
f.puts
|
61
|
+
bag_pathname.join('bagit.txt').open('w') do |f|
|
62
|
+
f.puts 'Tag-File-Character-Encoding: UTF-8'
|
63
|
+
f.puts 'BagIt-Version: 0.97'
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
@@ -209,7 +209,7 @@ module Moab
|
|
209
209
|
if manifest_file[type]
|
210
210
|
manifest_file[type].close
|
211
211
|
manifest_pathname[type].delete if
|
212
|
-
manifest_pathname[type].exist? && manifest_pathname[type].
|
212
|
+
manifest_pathname[type].exist? && manifest_pathname[type].empty?
|
213
213
|
end
|
214
214
|
end
|
215
215
|
end
|
@@ -217,7 +217,7 @@ module Moab
|
|
217
217
|
# @api internal
|
218
218
|
# @return [void] Generate the bag-info.txt tag file
|
219
219
|
def create_bag_info_txt
|
220
|
-
bag_pathname.join(
|
220
|
+
bag_pathname.join('bag-info.txt').open('w') do |f|
|
221
221
|
f.puts "External-Identifier: #{bag_inventory.package_id}"
|
222
222
|
f.puts "Payload-Oxum: #{bag_inventory.byte_count}.#{bag_inventory.file_count}"
|
223
223
|
f.puts "Bag-Size: #{bag_inventory.human_size}"
|
@@ -247,7 +247,7 @@ module Moab
|
|
247
247
|
if manifest_file[type]
|
248
248
|
manifest_file[type].close
|
249
249
|
manifest_pathname[type].delete if
|
250
|
-
manifest_pathname[type].exist? && manifest_pathname[type].
|
250
|
+
manifest_pathname[type].exist? && manifest_pathname[type].empty?
|
251
251
|
end
|
252
252
|
end
|
253
253
|
end
|
data/lib/moab/file_group.rb
CHANGED
@@ -23,7 +23,7 @@ module Moab
|
|
23
23
|
# (see Serializable#initialize)
|
24
24
|
def initialize(opts = {})
|
25
25
|
@signature_hash = {}
|
26
|
-
@data_source =
|
26
|
+
@data_source = ''
|
27
27
|
@signatures_from_bag = nil # prevents later warning: instance variable @signatures_from_bag not initialized
|
28
28
|
super(opts)
|
29
29
|
end
|
@@ -158,14 +158,16 @@ module Moab
|
|
158
158
|
end
|
159
159
|
attr_reader :base_directory
|
160
160
|
|
161
|
+
# FIXME: shouldn't this method be named descendent_of_base?
|
161
162
|
# @api internal
|
162
163
|
# @param pathname [Pathname] The file path to be tested
|
163
164
|
# @return [Boolean] Test whether the given path is contained within the {#base_directory}
|
164
165
|
def is_descendent_of_base?(pathname)
|
165
|
-
raise(MoabRuntimeError,
|
166
|
+
raise(MoabRuntimeError, 'base_directory has not been set') if @base_directory.nil?
|
166
167
|
|
167
168
|
is_descendent = false
|
168
169
|
pathname.expand_path.ascend { |ancestor| is_descendent ||= (ancestor == @base_directory) }
|
170
|
+
# FIXME: shouldn't it simply return false?
|
169
171
|
raise(MoabRuntimeError, "#{pathname} is not a descendent of #{@base_directory}") unless is_descendent
|
170
172
|
|
171
173
|
is_descendent
|
@@ -233,7 +233,7 @@ module Moab
|
|
233
233
|
matching_paths.each do |path|
|
234
234
|
fid = FileInstanceDifference.new(change: 'identical')
|
235
235
|
fid.basis_path = path
|
236
|
-
fid.other_path =
|
236
|
+
fid.other_path = 'same'
|
237
237
|
fid.signatures << signature
|
238
238
|
subset_hash[:identical].files << fid
|
239
239
|
end
|
@@ -286,7 +286,7 @@ module Moab
|
|
286
286
|
matching_keys(basis_path_hash, other_path_hash).each do |path|
|
287
287
|
fid = FileInstanceDifference.new(change: 'modified')
|
288
288
|
fid.basis_path = path
|
289
|
-
fid.other_path =
|
289
|
+
fid.other_path = 'same'
|
290
290
|
fid.signatures << basis_path_hash[path]
|
291
291
|
fid.signatures << other_path_hash[path]
|
292
292
|
subset_hash[:modified].files << fid
|
@@ -304,7 +304,7 @@ module Moab
|
|
304
304
|
def tabulate_added_files(basis_path_hash, other_path_hash)
|
305
305
|
other_only_keys(basis_path_hash, other_path_hash).each do |path|
|
306
306
|
fid = FileInstanceDifference.new(change: 'added')
|
307
|
-
fid.basis_path =
|
307
|
+
fid.basis_path = ''
|
308
308
|
fid.other_path = path
|
309
309
|
fid.signatures << other_path_hash[path]
|
310
310
|
subset_hash[:added].files << fid
|
@@ -323,7 +323,7 @@ module Moab
|
|
323
323
|
basis_only_keys(basis_path_hash, other_path_hash).each do |path|
|
324
324
|
fid = FileInstanceDifference.new(change: 'deleted')
|
325
325
|
fid.basis_path = path
|
326
|
-
fid.other_path =
|
326
|
+
fid.other_path = ''
|
327
327
|
fid.signatures << basis_path_hash[path]
|
328
328
|
subset_hash[:deleted].files << fid
|
329
329
|
end
|
data/lib/moab/file_inventory.rb
CHANGED
@@ -234,9 +234,11 @@ module Moab
|
|
234
234
|
count += 1
|
235
235
|
end
|
236
236
|
if count == 0
|
237
|
-
format(
|
237
|
+
format('%d B', size)
|
238
238
|
else
|
239
|
-
|
239
|
+
# rubocop:disable Style/FormatStringToken
|
240
|
+
format('%.2f %s', size, %w[B KB MB GB TB][count])
|
241
|
+
# rubocop:enable Style/FormatStringToken
|
240
242
|
end
|
241
243
|
end
|
242
244
|
|
@@ -245,13 +247,13 @@ module Moab
|
|
245
247
|
# @return [String] The standard name for the serialized inventory file of the given type
|
246
248
|
def self.xml_filename(type = nil)
|
247
249
|
case type
|
248
|
-
when
|
250
|
+
when 'version'
|
249
251
|
'versionInventory.xml'
|
250
|
-
when
|
252
|
+
when 'additions'
|
251
253
|
'versionAdditions.xml'
|
252
|
-
when
|
254
|
+
when 'manifests'
|
253
255
|
'manifestInventory.xml'
|
254
|
-
when
|
256
|
+
when 'directory'
|
255
257
|
'directoryInventory.xml'
|
256
258
|
else
|
257
259
|
raise ArgumentError, "unknown inventory type: #{type}"
|
@@ -109,18 +109,18 @@ module Moab
|
|
109
109
|
|
110
110
|
# @return [Hash] Serializes the data and then filters it to report only the changes
|
111
111
|
def differences_detail
|
112
|
-
#return self.summary if difference_count == 0
|
112
|
+
# return self.summary if difference_count == 0
|
113
113
|
inv_diff = to_hash
|
114
|
-
inv_diff[
|
114
|
+
inv_diff['group_differences'].each_value do |group_diff|
|
115
115
|
delete_subsets = []
|
116
|
-
group_diff[
|
117
|
-
delete_subsets << change_type if (change_type ==
|
116
|
+
group_diff['subsets'].each do |change_type, subset|
|
117
|
+
delete_subsets << change_type if (change_type == 'identical') || (subset['count'] == 0)
|
118
118
|
end
|
119
119
|
delete_subsets.each do |change_type|
|
120
|
-
group_diff[
|
121
|
-
group_diff.delete(change_type) if change_type !=
|
120
|
+
group_diff['subsets'].delete(change_type)
|
121
|
+
group_diff.delete(change_type) if change_type != 'identical'
|
122
122
|
end
|
123
|
-
group_diff.delete(
|
123
|
+
group_diff.delete('subsets') if group_diff['subsets'].empty?
|
124
124
|
end
|
125
125
|
inv_diff
|
126
126
|
end
|
data/lib/moab/file_signature.rb
CHANGED
@@ -50,15 +50,15 @@ module Moab
|
|
50
50
|
|
51
51
|
# @attribute
|
52
52
|
# @return [String] The MD5 checksum value of the file
|
53
|
-
attribute :md5, String, on_save: proc { |n| n.nil? ?
|
53
|
+
attribute :md5, String, on_save: proc { |n| n.nil? ? '' : n.to_s }
|
54
54
|
|
55
55
|
# @attribute
|
56
56
|
# @return [String] The SHA1 checksum value of the file
|
57
|
-
attribute :sha1, String, on_save: proc { |n| n.nil? ?
|
57
|
+
attribute :sha1, String, on_save: proc { |n| n.nil? ? '' : n.to_s }
|
58
58
|
|
59
59
|
# @attribute
|
60
60
|
# @return [String] The SHA256 checksum value of the file
|
61
|
-
attribute :sha256, String, on_save: proc { |n| n.nil? ?
|
61
|
+
attribute :sha256, String, on_save: proc { |n| n.nil? ? '' : n.to_s }
|
62
62
|
|
63
63
|
KNOWN_ALGOS = {
|
64
64
|
md5: proc { Digest::MD5.new },
|
@@ -79,7 +79,7 @@ module Moab
|
|
79
79
|
|
80
80
|
signatures = algos_to_use.to_h { |k| [k, KNOWN_ALGOS[k].call] }
|
81
81
|
|
82
|
-
pathname.open(
|
82
|
+
pathname.open('r') do |stream|
|
83
83
|
while (buffer = stream.read(8192))
|
84
84
|
signatures.each_value { |digest| digest.update(buffer) }
|
85
85
|
end
|
data/lib/moab/storage_object.rb
CHANGED
@@ -124,7 +124,7 @@ module Moab
|
|
124
124
|
# @param version_id [Integer] The version identifier of an object version
|
125
125
|
# @return [String] The directory name of the version, relative to the digital object home directory (e.g v0002)
|
126
126
|
def self.version_dirname(version_id)
|
127
|
-
format(
|
127
|
+
format('v%04d', version_id)
|
128
128
|
end
|
129
129
|
|
130
130
|
# @return [Array<Integer>] The list of all version ids for this object
|
@@ -193,7 +193,7 @@ module Moab
|
|
193
193
|
# * Version 0 is a special case used to generate empty manifests
|
194
194
|
# * Current version + 1 is used for creation of a new version
|
195
195
|
def storage_object_version(version_id)
|
196
|
-
raise(MoabRuntimeError,
|
196
|
+
raise(MoabRuntimeError, 'Version ID not specified') unless version_id
|
197
197
|
|
198
198
|
StorageObjectVersion.new(self, version_id)
|
199
199
|
end
|
@@ -6,17 +6,17 @@ module Moab
|
|
6
6
|
# Given a druid path, are the contents actually a well-formed Moab?
|
7
7
|
# Shameless green: repetitious code included.
|
8
8
|
class StorageObjectValidator
|
9
|
-
METADATA_DIR =
|
10
|
-
CONTENT_DIR =
|
9
|
+
METADATA_DIR = 'metadata'
|
10
|
+
CONTENT_DIR = 'content'
|
11
11
|
EXPECTED_DATA_SUB_DIRS = [CONTENT_DIR, METADATA_DIR].freeze
|
12
12
|
IMPLICIT_DIRS = ['.', '..'].freeze # unlike Find.find, Dir.entries returns the current/parent dirs
|
13
|
-
DATA_DIR =
|
13
|
+
DATA_DIR = 'data'
|
14
14
|
MANIFESTS_DIR = 'manifests'
|
15
15
|
EXPECTED_VERSION_SUB_DIRS = [DATA_DIR, MANIFESTS_DIR].freeze
|
16
|
-
MANIFEST_INVENTORY_PATH = File.join(MANIFESTS_DIR,
|
17
|
-
SIGNATURE_CATALOG_PATH = File.join(MANIFESTS_DIR,
|
16
|
+
MANIFEST_INVENTORY_PATH = File.join(MANIFESTS_DIR, 'manifestInventory.xml').freeze
|
17
|
+
SIGNATURE_CATALOG_PATH = File.join(MANIFESTS_DIR, 'signatureCatalog.xml').freeze
|
18
18
|
|
19
|
-
|
19
|
+
VERSION_DIR_PATTERN = /^v\d{4}$/
|
20
20
|
|
21
21
|
# error codes
|
22
22
|
INCORRECT_DIR_CONTENTS = 0
|
@@ -26,13 +26,12 @@ module Moab
|
|
26
26
|
NO_SIGNATURE_CATALOG = 4
|
27
27
|
NO_MANIFEST_INVENTORY = 5
|
28
28
|
NO_FILES_IN_MANIFEST_DIR = 6
|
29
|
-
|
29
|
+
TEST_OBJECT_VERSIONS_NOT_IN_ORDER = 7
|
30
30
|
METADATA_SUB_DIRS_DETECTED = 8
|
31
31
|
FILES_IN_VERSION_DIR = 9
|
32
32
|
NO_FILES_IN_METADATA_DIR = 10
|
33
33
|
NO_FILES_IN_CONTENT_DIR = 11
|
34
34
|
CONTENT_SUB_DIRS_DETECTED = 12
|
35
|
-
BAD_SUB_DIR_IN_CONTENT_DIR = 13
|
36
35
|
|
37
36
|
attr_reader :storage_obj_path
|
38
37
|
|
@@ -52,48 +51,43 @@ module Moab
|
|
52
51
|
def self.error_code_to_messages
|
53
52
|
@error_code_to_messages ||=
|
54
53
|
{
|
55
|
-
INCORRECT_DIR_CONTENTS =>
|
56
|
-
MISSING_DIR =>
|
57
|
-
EXTRA_CHILD_DETECTED =>
|
58
|
-
VERSION_DIR_BAD_FORMAT => "Version directory name not in 'v00xx' format:
|
59
|
-
FILES_IN_VERSION_DIR =>
|
60
|
-
NO_SIGNATURE_CATALOG =>
|
61
|
-
NO_MANIFEST_INVENTORY =>
|
62
|
-
NO_FILES_IN_MANIFEST_DIR =>
|
63
|
-
METADATA_SUB_DIRS_DETECTED =>
|
64
|
-
|
65
|
-
NO_FILES_IN_METADATA_DIR =>
|
66
|
-
NO_FILES_IN_CONTENT_DIR =>
|
67
|
-
CONTENT_SUB_DIRS_DETECTED =>
|
68
|
-
BAD_SUB_DIR_IN_CONTENT_DIR => "Version %{addl}: content directory has forbidden sub-directory name: vnnnn or #{FORBIDDEN_CONTENT_SUB_DIRS}"
|
54
|
+
INCORRECT_DIR_CONTENTS => 'Incorrect items under %<addl>s directory',
|
55
|
+
MISSING_DIR => 'Missing directory: %<addl>s',
|
56
|
+
EXTRA_CHILD_DETECTED => 'Unexpected item in path: %<addl>s',
|
57
|
+
VERSION_DIR_BAD_FORMAT => "Version directory name not in 'v00xx' format: %<addl>s",
|
58
|
+
FILES_IN_VERSION_DIR => 'Version directory %<addl>s should not contain files; only the manifests and data directories',
|
59
|
+
NO_SIGNATURE_CATALOG => 'Version %<addl>s: Missing signatureCatalog.xml',
|
60
|
+
NO_MANIFEST_INVENTORY => 'Version %<addl>s: Missing manifestInventory.xml',
|
61
|
+
NO_FILES_IN_MANIFEST_DIR => 'Version %<addl>s: No files present in manifest dir',
|
62
|
+
METADATA_SUB_DIRS_DETECTED => 'Version %<version>s: metadata directory should only contain files, not directories. Found directory: %<dir>s',
|
63
|
+
TEST_OBJECT_VERSIONS_NOT_IN_ORDER => 'Should contain only sequential version directories. Current directories: %<addl>s',
|
64
|
+
NO_FILES_IN_METADATA_DIR => 'Version %<addl>s: No files present in metadata dir',
|
65
|
+
NO_FILES_IN_CONTENT_DIR => 'Version %<addl>s: No files present in content dir',
|
66
|
+
CONTENT_SUB_DIRS_DETECTED => 'Version %<version>s: content directory should only contain files, not directories. Found directory: %<dir>s'
|
69
67
|
}.freeze
|
70
68
|
end
|
71
69
|
|
72
70
|
private
|
73
71
|
|
74
72
|
def version_directories
|
75
|
-
@
|
73
|
+
@version_directories ||= directory_entries(storage_obj_path)
|
76
74
|
end
|
77
75
|
|
78
76
|
def check_correctly_named_version_dirs
|
79
77
|
errors = []
|
80
78
|
errors << result_hash(MISSING_DIR, 'no versions exist') unless version_directories.count > 0
|
81
79
|
version_directories.each do |version_dir|
|
82
|
-
errors << result_hash(VERSION_DIR_BAD_FORMAT, version_dir) unless
|
80
|
+
errors << result_hash(VERSION_DIR_BAD_FORMAT, version_dir) unless VERSION_DIR_PATTERN.match?(version_dir)
|
83
81
|
end
|
84
82
|
errors
|
85
83
|
end
|
86
84
|
|
87
|
-
def version_dir_format?(dirname)
|
88
|
-
dirname =~ /^v\d{4}$/
|
89
|
-
end
|
90
|
-
|
91
85
|
# call only if the version directories are "correctly named" vdddd
|
92
86
|
def check_sequential_version_dirs
|
93
87
|
version_directories.each_with_index do |dir_name, index|
|
94
88
|
next if dir_name[1..].to_i == index + 1 # version numbering starts at 1, array indexing at 0
|
95
89
|
|
96
|
-
return [result_hash(
|
90
|
+
return [result_hash(TEST_OBJECT_VERSIONS_NOT_IN_ORDER, version_directories)]
|
97
91
|
end
|
98
92
|
[]
|
99
93
|
end
|
@@ -151,20 +145,9 @@ module Moab
|
|
151
145
|
if content_sub_dir && !allow_content_subdirs
|
152
146
|
errors << result_hash(CONTENT_SUB_DIRS_DETECTED, version: basename(version_path), dir: content_sub_dir)
|
153
147
|
end
|
154
|
-
if allow_content_subdirs && contains_sub_dir?(content_dir_path) && contains_forbidden_content_sub_dir?(content_dir_path)
|
155
|
-
errors << result_hash(BAD_SUB_DIR_IN_CONTENT_DIR, basename(version_path))
|
156
|
-
end
|
157
148
|
errors
|
158
149
|
end
|
159
150
|
|
160
|
-
def contains_forbidden_content_sub_dir?(path)
|
161
|
-
sub_dirs(path).detect { |sub_dir| content_sub_dir_forbidden?(sub_dir) }
|
162
|
-
end
|
163
|
-
|
164
|
-
def content_sub_dir_forbidden?(dirname)
|
165
|
-
FORBIDDEN_CONTENT_SUB_DIRS.include?(dirname) || version_dir_format?(dirname)
|
166
|
-
end
|
167
|
-
|
168
151
|
def check_metadata_dir_files_only(version_path)
|
169
152
|
errors = []
|
170
153
|
metadata_dir_path = File.join(version_path, DATA_DIR, METADATA_DIR)
|
@@ -174,7 +174,7 @@ module Moab
|
|
174
174
|
# @return [void] link or copy the specified file from source location to the version directory
|
175
175
|
def ingest_file(source_file, target_dir, use_links = true)
|
176
176
|
if use_links
|
177
|
-
FileUtils.link(source_file.to_s, target_dir.to_s)
|
177
|
+
FileUtils.link(source_file.to_s, target_dir.to_s)
|
178
178
|
else
|
179
179
|
FileUtils.copy(source_file.to_s, target_dir.to_s)
|
180
180
|
end
|
@@ -225,7 +225,7 @@ module Moab
|
|
225
225
|
# @return [VerificationResult] return true if the manifest inventory matches the actual files
|
226
226
|
def verify_manifest_inventory
|
227
227
|
# read/parse manifestInventory.xml
|
228
|
-
result = VerificationResult.new(
|
228
|
+
result = VerificationResult.new('manifest_inventory')
|
229
229
|
manifest_inventory = file_inventory('manifests')
|
230
230
|
result.subentities << VerificationResult.verify_value('composite_key', composite_key, manifest_inventory.composite_key)
|
231
231
|
result.subentities << VerificationResult.verify_truth('manifests_group', !manifest_inventory.group_empty?('manifests'))
|
@@ -233,7 +233,7 @@ module Moab
|
|
233
233
|
directory_inventory = FileInventory.new.inventory_from_directory(@version_pathname.join('manifests'), 'manifests')
|
234
234
|
directory_inventory.digital_object_id = storage_object.digital_object_id
|
235
235
|
directory_group = directory_inventory.group('manifests')
|
236
|
-
directory_group.remove_file_having_path(
|
236
|
+
directory_group.remove_file_having_path('manifestInventory.xml')
|
237
237
|
# compare the measured signatures against the values in manifestInventory.xml
|
238
238
|
diff = FileInventoryDifference.new
|
239
239
|
diff.compare(manifest_inventory, directory_inventory)
|
@@ -247,7 +247,7 @@ module Moab
|
|
247
247
|
|
248
248
|
# @return [VerificationResult]
|
249
249
|
def verify_signature_catalog
|
250
|
-
result = VerificationResult.new(
|
250
|
+
result = VerificationResult.new('signature_catalog')
|
251
251
|
signature_catalog = self.signature_catalog
|
252
252
|
result.subentities << VerificationResult.verify_value('signature_key', composite_key, signature_catalog.composite_key)
|
253
253
|
found = 0
|
@@ -261,7 +261,7 @@ module Moab
|
|
261
261
|
missing << storage_location.to_s
|
262
262
|
end
|
263
263
|
end
|
264
|
-
file_result = VerificationResult.new(
|
264
|
+
file_result = VerificationResult.new('storage_location')
|
265
265
|
file_result.verified = (found == signature_catalog.file_count)
|
266
266
|
file_result.details = {
|
267
267
|
'expected' => signature_catalog.file_count,
|
@@ -275,7 +275,7 @@ module Moab
|
|
275
275
|
|
276
276
|
# @return [Boolean] true if files & signatures listed in version inventory can all be found
|
277
277
|
def verify_version_inventory
|
278
|
-
result = VerificationResult.new(
|
278
|
+
result = VerificationResult.new('version_inventory')
|
279
279
|
version_inventory = file_inventory('version')
|
280
280
|
result.subentities << VerificationResult.verify_value('inventory_key', composite_key, version_inventory.composite_key)
|
281
281
|
signature_catalog = self.signature_catalog
|
@@ -295,7 +295,7 @@ module Moab
|
|
295
295
|
end
|
296
296
|
end
|
297
297
|
end
|
298
|
-
file_result = VerificationResult.new(
|
298
|
+
file_result = VerificationResult.new('catalog_entry')
|
299
299
|
file_result.verified = (found == version_inventory.file_count)
|
300
300
|
file_result.details = {
|
301
301
|
'expected' => version_inventory.file_count,
|
@@ -309,7 +309,7 @@ module Moab
|
|
309
309
|
|
310
310
|
# @return [Boolean] returns true if files in data folder match files listed in version addtions inventory
|
311
311
|
def verify_version_additions
|
312
|
-
result = VerificationResult.new(
|
312
|
+
result = VerificationResult.new('version_additions')
|
313
313
|
version_additions = file_inventory('additions')
|
314
314
|
result.subentities << VerificationResult.verify_value('composite_key', composite_key, version_additions.composite_key)
|
315
315
|
data_directory = @version_pathname.join('data')
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'find' # for object_size
|
4
|
+
|
3
5
|
module Moab
|
4
6
|
# A class to represent the SDR repository store
|
5
7
|
#
|
@@ -20,10 +22,10 @@ module Moab
|
|
20
22
|
# @return [Array<Pathname>] The list of filesystem root paths in which objects are stored
|
21
23
|
def storage_roots
|
22
24
|
unless defined?(@storage_roots)
|
23
|
-
raise(MoabRuntimeError,
|
25
|
+
raise(MoabRuntimeError, 'Moab::Config.storage_roots not found in config file') if Moab::Config.storage_roots.nil?
|
24
26
|
|
25
27
|
@storage_roots = [Moab::Config.storage_roots].flatten.collect { |filesystem| Pathname(filesystem) }
|
26
|
-
raise(MoabRuntimeError,
|
28
|
+
raise(MoabRuntimeError, 'Moab::Config.storage_roots empty') if @storage_roots.empty?
|
27
29
|
|
28
30
|
@storage_roots.each { |root| raise(MoabRuntimeError, "Storage root #{root} not found on system") unless root.exist? }
|
29
31
|
end
|
@@ -33,7 +35,7 @@ module Moab
|
|
33
35
|
# @return [String] The trunk segment of the object storage path
|
34
36
|
def storage_trunk
|
35
37
|
unless defined?(@storage_trunk)
|
36
|
-
raise(MoabRuntimeError,
|
38
|
+
raise(MoabRuntimeError, 'Moab::Config.storage_trunk not found in config file') if Moab::Config.storage_trunk.nil?
|
37
39
|
|
38
40
|
@storage_trunk = Moab::Config.storage_trunk
|
39
41
|
end
|
@@ -43,7 +45,7 @@ module Moab
|
|
43
45
|
# @param object_id [String] The identifier of the digital object
|
44
46
|
# @return [String] The branch segment of the object storage path
|
45
47
|
def storage_branch(object_id)
|
46
|
-
#
|
48
|
+
# TODO: This method should be customized, or overridden in a subclass
|
47
49
|
# split a object ID into 2-character segments, followed by a copy of the object ID
|
48
50
|
# for a more sophisticated pairtree implementation see https://github.com/microservices/pairtree
|
49
51
|
path_segments = object_id.scan(/..?/) << object_id
|
@@ -103,34 +105,6 @@ module Moab
|
|
103
105
|
create_storage_object(object_id, root)
|
104
106
|
end
|
105
107
|
|
106
|
-
# @param object_id [String] The identifier of the digital object
|
107
|
-
# @param include_deposit [Boolean] specifies whether to look in deposit areas for objects in process of initial ingest
|
108
|
-
# @return [Array<StorageObject>] Representations of a digitial object's storage directories, or an empty array if none found.
|
109
|
-
def search_storage_objects(object_id, include_deposit = false)
|
110
|
-
storage_objects = []
|
111
|
-
# Search for the object's home directory in the storage areas
|
112
|
-
branch = storage_branch(object_id)
|
113
|
-
storage_roots.each do |root|
|
114
|
-
root_trunk = root.join(storage_trunk)
|
115
|
-
raise(MoabRuntimeError, "Storage area not found at #{root_trunk}") unless root_trunk.exist?
|
116
|
-
|
117
|
-
root_trunk_branch = root_trunk.join(branch)
|
118
|
-
storage_objects << create_storage_object(object_id, root) if root_trunk_branch.exist?
|
119
|
-
end
|
120
|
-
# Search for the object's directory in the deposit areas
|
121
|
-
if include_deposit && deposit_trunk
|
122
|
-
branch = deposit_branch(object_id)
|
123
|
-
storage_roots.each do |root|
|
124
|
-
root_trunk = root.join(deposit_trunk)
|
125
|
-
raise(MoabRuntimeError, "Deposit area not found at #{root_trunk}") unless root_trunk.exist?
|
126
|
-
|
127
|
-
root_trunk_branch = root_trunk.join(branch)
|
128
|
-
storage_objects << create_storage_object(object_id, root) if root_trunk_branch.exist?
|
129
|
-
end
|
130
|
-
end
|
131
|
-
storage_objects
|
132
|
-
end
|
133
|
-
|
134
108
|
# @param object_id [String] The identifier of the digital object whose size is desired
|
135
109
|
# @param include_deposit [Boolean] specifies whether to look in deposit areas for objects in process of initial ingest
|
136
110
|
# @return [Integer] the size occupied on disk by the storage object, in bytes. this is the entire moab (all versions).
|
@@ -13,81 +13,83 @@ module Moab
|
|
13
13
|
# @note Copyright (c) 2012 by The Board of Trustees of the Leland Stanford Junior University.
|
14
14
|
# All rights reserved. See {file:LICENSE.rdoc} for details.
|
15
15
|
class StorageServices
|
16
|
-
# @
|
16
|
+
# @note After some discussion, consensus was that this is a thread safe use of a
|
17
|
+
# class variable, as 1) it's never mutated after the class is initialized, and 2) the
|
18
|
+
# value of the StorageRepository instance is determined from configuration that
|
19
|
+
# rarely changes and is loaded once at app start time (at least in Stanford's
|
20
|
+
# consumers; see Moab::Config.configure calls in preservation_robots, preservation_catalog,
|
21
|
+
# and technical-metadata-service).
|
22
|
+
# Sidekiq requires thread safe code, so please preserve thread safety for multiple
|
23
|
+
# concurrent callers of this service if refactoring, so Sidekiq remains an option for
|
24
|
+
# ActiveJob backend for moab-versioning consumers.
|
17
25
|
@@repository = Moab::StorageRepository.new
|
18
26
|
|
27
|
+
# @return [StorageRepository] an instance of the interface to SDR storage
|
19
28
|
def self.repository
|
20
29
|
@@repository
|
21
30
|
end
|
22
31
|
|
23
32
|
# @return [Array<Pathname>] A list of the filesystems currently used for storage
|
24
33
|
def self.storage_roots
|
25
|
-
|
34
|
+
repository.storage_roots
|
26
35
|
end
|
27
36
|
|
28
37
|
# @return [String] The trunk segment of the object deposit path
|
29
38
|
def self.deposit_trunk
|
30
|
-
|
39
|
+
repository.deposit_trunk
|
31
40
|
end
|
32
41
|
|
33
42
|
# @param object_id [String] The identifier of the digital object
|
34
43
|
# @return [Pathname] The branch segment of the object deposit path
|
35
44
|
def self.deposit_branch(object_id)
|
36
|
-
|
45
|
+
repository.deposit_branch(object_id)
|
37
46
|
end
|
38
47
|
|
39
48
|
# @param object_id [String] The identifier of the digital object
|
40
49
|
# @param [Object] include_deposit
|
41
50
|
# @return [StorageObject] The representation of a digitial object's storage directory, which might not exist yet.
|
42
51
|
def self.find_storage_object(object_id, include_deposit = false)
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
# @param object_id [String] The identifier of the digital object
|
47
|
-
# @param [Object] include_deposit
|
48
|
-
# @return [Array<StorageObject>] Representations of a digitial object's storage directories, or an empty array if none found.
|
49
|
-
def self.search_storage_objects(object_id, include_deposit = false)
|
50
|
-
@@repository.search_storage_objects(object_id, include_deposit)
|
52
|
+
repository.find_storage_object(object_id, include_deposit)
|
51
53
|
end
|
52
54
|
|
53
55
|
# @param object_id [String] The identifier of the digital object whose size is desired
|
54
56
|
# @param include_deposit [Boolean] specifies whether to look in deposit areas for objects in process of initial ingest
|
55
57
|
# @return [Integer] the size occupied on disk by the storage object, in bytes. this is the entire moab (all versions).
|
56
58
|
def self.object_size(object_id, include_deposit = false)
|
57
|
-
|
59
|
+
repository.object_size(object_id, include_deposit)
|
58
60
|
end
|
59
61
|
|
60
62
|
# @param object_id [String] The identifier of the digital object whose version is desired
|
61
63
|
# @param create [Boolean] If true, the object home directory should be created if it does not exist
|
62
64
|
# @return [StorageObject] The representation of a digitial object's storage directory, which must exist.
|
63
65
|
def self.storage_object(object_id, create = false)
|
64
|
-
|
66
|
+
repository.storage_object(object_id, create)
|
65
67
|
end
|
66
68
|
|
67
69
|
# @param object_id [String] The digital object identifier of the object
|
68
70
|
# @return [String] the location of the storage object
|
69
71
|
def self.object_path(object_id)
|
70
|
-
|
72
|
+
repository.storage_object(object_id).object_pathname.to_s
|
71
73
|
end
|
72
74
|
|
73
75
|
# @param object_id [String] The digital object identifier of the object
|
74
76
|
# @param [Integer] version_id The ID of the version, if nil use latest version
|
75
77
|
# @return [String] the location of the storage object version
|
76
78
|
def self.object_version_path(object_id, version_id = nil)
|
77
|
-
|
79
|
+
repository.storage_object(object_id).find_object_version(version_id).version_pathname.to_s
|
78
80
|
end
|
79
81
|
|
80
82
|
# @param object_id [String] The digital object identifier
|
81
83
|
# @return [Integer] The version number of the currently highest version
|
82
84
|
def self.current_version(object_id)
|
83
|
-
|
85
|
+
repository.storage_object(object_id).current_version_id
|
84
86
|
end
|
85
87
|
|
86
88
|
# @param [String] object_id The digital object identifier of the object
|
87
89
|
# @param [Integer] version_id The ID of the version, if nil use latest version
|
88
90
|
# @return [FileInventory] the file inventory for the specified object version
|
89
91
|
def self.retrieve_file_group(file_category, object_id, version_id = nil)
|
90
|
-
storage_object_version =
|
92
|
+
storage_object_version = repository.storage_object(object_id).find_object_version(version_id)
|
91
93
|
inventory_type = if file_category =~ /manifest/
|
92
94
|
file_category = 'manifests'
|
93
95
|
else
|
@@ -103,7 +105,7 @@ module Moab
|
|
103
105
|
# @param [Integer] version_id The ID of the version, if nil use latest version
|
104
106
|
# @return [Pathname] Pathname object containing the full path for the specified file
|
105
107
|
def self.retrieve_file(file_category, file_id, object_id, version_id = nil)
|
106
|
-
storage_object_version =
|
108
|
+
storage_object_version = repository.storage_object(object_id).find_object_version(version_id)
|
107
109
|
storage_object_version.find_filepath(file_category, file_id)
|
108
110
|
end
|
109
111
|
|
@@ -113,7 +115,7 @@ module Moab
|
|
113
115
|
# @param [Integer] version_id The ID of the version, if nil use latest version
|
114
116
|
# @return [Pathname] Pathname object containing the full path for the specified file
|
115
117
|
def self.retrieve_file_using_signature(file_category, file_signature, object_id, version_id = nil)
|
116
|
-
storage_object_version =
|
118
|
+
storage_object_version = repository.storage_object(object_id).find_object_version(version_id)
|
117
119
|
storage_object_version.find_filepath_using_signature(file_category, file_signature)
|
118
120
|
end
|
119
121
|
|
@@ -123,7 +125,7 @@ module Moab
|
|
123
125
|
# @param [Integer] version_id The ID of the version, if nil use latest version
|
124
126
|
# @return [FileSignature] The signature of the file
|
125
127
|
def self.retrieve_file_signature(file_category, file_id, object_id, version_id = nil)
|
126
|
-
storage_object_version =
|
128
|
+
storage_object_version = repository.storage_object(object_id).find_object_version(version_id)
|
127
129
|
storage_object_version.find_signature(file_category, file_id)
|
128
130
|
end
|
129
131
|
|
@@ -132,8 +134,8 @@ module Moab
|
|
132
134
|
# @param [Object] compare_version_id The identifier of the version to be compared to the base version
|
133
135
|
# @return [FileInventoryDifference] The report of the version differences
|
134
136
|
def self.version_differences(object_id, base_version_id, compare_version_id)
|
135
|
-
base_version =
|
136
|
-
compare_version =
|
137
|
+
base_version = repository.storage_object(object_id).storage_object_version(base_version_id)
|
138
|
+
compare_version = repository.storage_object(object_id).storage_object_version(compare_version_id)
|
137
139
|
base_inventory = base_version.file_inventory('version')
|
138
140
|
compare_inventory = compare_version.file_inventory('version')
|
139
141
|
FileInventoryDifference.new.compare(base_inventory, compare_inventory)
|
data/lib/moab/utc_time.rb
CHANGED
@@ -7,7 +7,7 @@ module Moab
|
|
7
7
|
# @return [void] Convert input datetime to a Time object, or nil if input is empty.
|
8
8
|
def self.input(datetime)
|
9
9
|
case datetime
|
10
|
-
when nil,
|
10
|
+
when nil, ''
|
11
11
|
nil
|
12
12
|
when String
|
13
13
|
Time.parse(datetime)
|
@@ -22,8 +22,8 @@ module Moab
|
|
22
22
|
# @return [String] Convert the datetime into a ISO 8601 formatted string
|
23
23
|
def self.output(datetime)
|
24
24
|
case datetime
|
25
|
-
when nil,
|
26
|
-
|
25
|
+
when nil, ''
|
26
|
+
''
|
27
27
|
when String
|
28
28
|
Time.parse(datetime).utc.iso8601
|
29
29
|
when Time
|
@@ -22,7 +22,7 @@ module Moab
|
|
22
22
|
@entity = entity.to_s # force to string
|
23
23
|
@verified = !!verified # rubocop:disable Style/DoubleNegation
|
24
24
|
@details = details
|
25
|
-
@subentities =
|
25
|
+
@subentities = []
|
26
26
|
end
|
27
27
|
|
28
28
|
# @param entity [#to_s] The name of the entity being verified
|
data/lib/serializer/manifest.rb
CHANGED
@@ -59,9 +59,9 @@ module Serializer
|
|
59
59
|
def self.write_xml_file(xml_object, parent_dir, filename = nil)
|
60
60
|
parent_dir.mkpath
|
61
61
|
xml_pathname(parent_dir, filename).open('w') do |f|
|
62
|
-
|
63
|
-
|
64
|
-
f <<
|
62
|
+
xml_builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8')
|
63
|
+
xml_builder = xml_object.to_xml(xml_builder)
|
64
|
+
f << xml_builder.to_xml
|
65
65
|
end
|
66
66
|
nil
|
67
67
|
end
|
@@ -120,7 +120,7 @@ module Serializer
|
|
120
120
|
# @param other [Serializable] The other object being compared
|
121
121
|
# @return [Hash] Generate a hash containing the differences between two objects of the same type
|
122
122
|
def diff(other)
|
123
|
-
raise(Moab::MoabRuntimeError,
|
123
|
+
raise(Moab::MoabRuntimeError, 'Cannot compare different classes') if self.class != other.class
|
124
124
|
|
125
125
|
left = other.to_hash
|
126
126
|
right = to_hash
|
data/lib/serializer.rb
CHANGED
@@ -23,7 +23,7 @@ module Stanford
|
|
23
23
|
# Many of these objects have contentMetadata with no child elements, such as this:
|
24
24
|
# <contentMetadata objectId="bd608mj3166" type="file"/>
|
25
25
|
# but there are also objects that have no datasteam of this name at all
|
26
|
-
cm_inventory = Moab::FileInventory.new(type:
|
26
|
+
cm_inventory = Moab::FileInventory.new(type: 'version', digital_object_id: object_id, version_id: version_id)
|
27
27
|
content_group = group_from_cm(content_metadata, subset)
|
28
28
|
cm_inventory.groups << content_group
|
29
29
|
cm_inventory
|
@@ -45,7 +45,7 @@ module Stanford
|
|
45
45
|
when 'shelve'
|
46
46
|
ng_doc.xpath("//file[@shelve='yes']")
|
47
47
|
when 'all'
|
48
|
-
ng_doc.xpath(
|
48
|
+
ng_doc.xpath('//file')
|
49
49
|
else
|
50
50
|
raise(Moab::MoabRuntimeError, "Unknown disposition subset (#{subset})")
|
51
51
|
end
|
@@ -86,7 +86,7 @@ module Stanford
|
|
86
86
|
instance.path = node.attributes['id'].content
|
87
87
|
instance.datetime = begin
|
88
88
|
node.attributes['datetime'].content
|
89
|
-
rescue
|
89
|
+
rescue StandardError
|
90
90
|
nil
|
91
91
|
end
|
92
92
|
instance
|
@@ -98,8 +98,8 @@ module Stanford
|
|
98
98
|
# @example {include:file:spec/features/stanford/content_metadata_write_spec.rb}
|
99
99
|
def generate_content_metadata(file_group, object_id, version_id)
|
100
100
|
cm = Nokogiri::XML::Builder.new do |xml|
|
101
|
-
xml.contentMetadata(type:
|
102
|
-
xml.resource(type:
|
101
|
+
xml.contentMetadata(type: 'sample', objectId: object_id) do
|
102
|
+
xml.resource(type: 'version', sequence: '1', id: "version-#{version_id}") do
|
103
103
|
file_group.files.each do |file_manifestation|
|
104
104
|
signature = file_manifestation.signature
|
105
105
|
file_manifestation.instances.each do |instance|
|
@@ -112,9 +112,9 @@ module Stanford
|
|
112
112
|
preserve: 'yes'
|
113
113
|
) do
|
114
114
|
fixity = signature.fixity
|
115
|
-
xml.checksum(type:
|
116
|
-
xml.checksum(type:
|
117
|
-
xml.checksum(type:
|
115
|
+
xml.checksum(type: 'MD5') { xml.text signature.md5 } if fixity[:md5]
|
116
|
+
xml.checksum(type: 'SHA-1') { xml.text signature.sha1 } if fixity[:sha1]
|
117
|
+
xml.checksum(type: 'SHA-256') { xml.text signature.sha256 } if fixity[:sha256]
|
118
118
|
end
|
119
119
|
end
|
120
120
|
end
|
@@ -139,16 +139,16 @@ module Stanford
|
|
139
139
|
result = []
|
140
140
|
content_metadata_doc =
|
141
141
|
case content_metadata.class.name
|
142
|
-
when
|
142
|
+
when 'String'
|
143
143
|
Nokogiri::XML(content_metadata)
|
144
|
-
when
|
144
|
+
when 'Pathname'
|
145
145
|
Nokogiri::XML(content_metadata.read)
|
146
|
-
when
|
146
|
+
when 'Nokogiri::XML::Document'
|
147
147
|
content_metadata
|
148
148
|
else
|
149
|
-
raise Moab::InvalidMetadataException,
|
149
|
+
raise Moab::InvalidMetadataException, 'Content Metadata is in unrecognized format'
|
150
150
|
end
|
151
|
-
nodeset = content_metadata_doc.xpath(
|
151
|
+
nodeset = content_metadata_doc.xpath('//file')
|
152
152
|
nodeset.each do |file_node|
|
153
153
|
missing = %w[id size md5 sha1]
|
154
154
|
missing.delete('id') if file_node.has_attribute?('id')
|
@@ -184,7 +184,7 @@ module Stanford
|
|
184
184
|
@type_for_name = Moab::FileSignature.checksum_type_for_name
|
185
185
|
@names_for_type = Moab::FileSignature.checksum_names_for_type
|
186
186
|
ng_doc = Nokogiri::XML(content_metadata, &:noblanks)
|
187
|
-
nodeset = ng_doc.xpath(
|
187
|
+
nodeset = ng_doc.xpath('//file')
|
188
188
|
nodeset.each do |file_node|
|
189
189
|
filepath = file_node['id']
|
190
190
|
signature = signature_for_path[filepath]
|
@@ -3,9 +3,22 @@
|
|
3
3
|
module Stanford
|
4
4
|
# An interface class to support access to SDR storage via a RESTful server
|
5
5
|
class StorageServices < Moab::StorageServices
|
6
|
-
# @
|
6
|
+
# @note After some discussion, consensus was that this is a thread safe use of a
|
7
|
+
# class variable, as 1) it's never mutated after the class is initialized, and 2) the
|
8
|
+
# value of the StorageRepository instance is determined from configuration that
|
9
|
+
# rarely changes and is loaded once at app start time (at least in Stanford's
|
10
|
+
# consumers; see Moab::Config.configure calls in preservation_robots, preservation_catalog,
|
11
|
+
# and technical-metadata-service).
|
12
|
+
# Sidekiq requires thread safe code, so please preserve thread safety for multiple
|
13
|
+
# concurrent callers of this service if refactoring, so Sidekiq remains an option for
|
14
|
+
# ActiveJob backend for moab-versioning consumers.
|
7
15
|
@@repository = Stanford::StorageRepository.new
|
8
16
|
|
17
|
+
# @return [StorageRepository] an instance of the interface to SDR storage
|
18
|
+
def self.repository
|
19
|
+
@@repository
|
20
|
+
end
|
21
|
+
|
9
22
|
# @param new_content_metadata [String] The content metadata to be compared to the base
|
10
23
|
# @param object_id [String] The digital object identifier of the object whose version inventory is the basis of the
|
11
24
|
# comparison
|
@@ -42,7 +55,7 @@ module Stanford
|
|
42
55
|
begin
|
43
56
|
# ObjectNotFoundException is raised if the object does not exist in storage
|
44
57
|
version_id ||= current_version(object_id)
|
45
|
-
storage_object_version =
|
58
|
+
storage_object_version = repository.storage_object(object_id).find_object_version(version_id)
|
46
59
|
signature_catalog = storage_object_version.signature_catalog
|
47
60
|
rescue Moab::ObjectNotFoundException
|
48
61
|
storage_object = Moab::StorageObject.new(object_id, 'dummy')
|
metadata
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: moab-versioning
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.0.alpha
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
8
|
-
-
|
9
|
-
-
|
10
|
-
-
|
11
|
-
|
7
|
+
- Naomi Dushay
|
8
|
+
- Justin Coyne
|
9
|
+
- Tony Zanella
|
10
|
+
- Mike Giarlo
|
11
|
+
- John Martin
|
12
|
+
autorequire:
|
12
13
|
bindir: bin
|
13
14
|
cert_chain: []
|
14
|
-
date:
|
15
|
+
date: 2023-01-17 00:00:00.000000000 Z
|
15
16
|
dependencies:
|
16
17
|
- !ruby/object:Gem::Dependency
|
17
18
|
name: druid-tools
|
@@ -169,7 +170,11 @@ dependencies:
|
|
169
170
|
version: '0'
|
170
171
|
description: Contains classes to process digital object version content and metadata
|
171
172
|
email:
|
172
|
-
-
|
173
|
+
- ndushay@stanford.edu
|
174
|
+
- jcoyne85@stanford.edu
|
175
|
+
- azanella@stanford.edu
|
176
|
+
- mjgiarlo@stanford.edu
|
177
|
+
- suntzu@stanford.edu
|
173
178
|
executables: []
|
174
179
|
extensions: []
|
175
180
|
extra_rdoc_files: []
|
@@ -210,7 +215,7 @@ licenses:
|
|
210
215
|
- Apache-2.0
|
211
216
|
metadata:
|
212
217
|
rubygems_mfa_required: 'true'
|
213
|
-
post_install_message:
|
218
|
+
post_install_message:
|
214
219
|
rdoc_options: []
|
215
220
|
require_paths:
|
216
221
|
- lib
|
@@ -218,16 +223,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
218
223
|
requirements:
|
219
224
|
- - ">="
|
220
225
|
- !ruby/object:Gem::Version
|
221
|
-
version: '
|
226
|
+
version: '3.0'
|
222
227
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
223
228
|
requirements:
|
224
|
-
- - "
|
229
|
+
- - ">"
|
225
230
|
- !ruby/object:Gem::Version
|
226
|
-
version:
|
231
|
+
version: 1.3.1
|
227
232
|
requirements: []
|
228
|
-
rubygems_version: 3.
|
229
|
-
signing_key:
|
233
|
+
rubygems_version: 3.2.32
|
234
|
+
signing_key:
|
230
235
|
specification_version: 4
|
231
|
-
summary: Ruby implementation of digital object versioning toolkit used by
|
232
|
-
|
236
|
+
summary: Ruby implementation of digital object versioning toolkit used by Stanford
|
237
|
+
University Libraries
|
233
238
|
test_files: []
|