moab-versioning 5.2.0 → 6.0.0.alpha
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/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: []
|