ocfl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +222 -0
- data/README.md +55 -0
- data/Rakefile +12 -0
- data/lib/ocfl/object/directory.rb +95 -0
- data/lib/ocfl/object/directory_builder.rb +55 -0
- data/lib/ocfl/object/draft_version.rb +90 -0
- data/lib/ocfl/object/inventory.rb +44 -0
- data/lib/ocfl/object/inventory_loader.rb +45 -0
- data/lib/ocfl/object/inventory_validator.rb +45 -0
- data/lib/ocfl/object/inventory_writer.rb +40 -0
- data/lib/ocfl/object/version.rb +22 -0
- data/lib/ocfl/object.rb +7 -0
- data/lib/ocfl/version.rb +5 -0
- data/lib/ocfl.rb +32 -0
- data/sig/ocfl.rbs +4 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1b78d16d5b12ad0e134c313c3c68b62c5d008534615d9e91f8d4acc0e4e46fbd
|
4
|
+
data.tar.gz: 513c7bce75abd89c7516208b2320fb7fd302a32e2c1052ea636d069f02eef721
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 575c5cc29088328fb7622bbf66c82779751952b11c6a6950e8e7d2e73eb43800cd187a5b681b3f2762dc0e4fa1aad88a6c63211bae1b94bcdf5f8986189d5e86
|
7
|
+
data.tar.gz: 05c1f6b0fd8819625b7fbc2209db416b2a84b6ae2383d18a1fefb5c963f3aa3e50322f1692dc009b45ec65c25e90d35c04b2911ea08b80ede22d41918158012c
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.1
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
Enabled: true
|
6
|
+
EnforcedStyle: double_quotes
|
7
|
+
|
8
|
+
Style/StringLiteralsInInterpolation:
|
9
|
+
Enabled: true
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
Layout/LineLength:
|
13
|
+
Max: 120
|
14
|
+
|
15
|
+
Metrics/BlockLength:
|
16
|
+
AllowedMethods:
|
17
|
+
- describe
|
18
|
+
- context
|
19
|
+
|
20
|
+
Style/StringConcatenation:
|
21
|
+
Enabled: false # Too many false positives with Pathname#+
|
22
|
+
|
23
|
+
Gemspec/DeprecatedAttributeAssignment: # new in 1.30
|
24
|
+
Enabled: true
|
25
|
+
Gemspec/DevelopmentDependencies: # new in 1.44
|
26
|
+
Enabled: true
|
27
|
+
Gemspec/RequireMFA: # new in 1.23
|
28
|
+
Enabled: true
|
29
|
+
Layout/LineContinuationLeadingSpace: # new in 1.31
|
30
|
+
Enabled: true
|
31
|
+
Layout/LineContinuationSpacing: # new in 1.31
|
32
|
+
Enabled: true
|
33
|
+
Layout/LineEndStringConcatenationIndentation: # new in 1.18
|
34
|
+
Enabled: true
|
35
|
+
Layout/SpaceBeforeBrackets: # new in 1.7
|
36
|
+
Enabled: true
|
37
|
+
Lint/AmbiguousAssignment: # new in 1.7
|
38
|
+
Enabled: true
|
39
|
+
Lint/AmbiguousOperatorPrecedence: # new in 1.21
|
40
|
+
Enabled: true
|
41
|
+
Lint/AmbiguousRange: # new in 1.19
|
42
|
+
Enabled: true
|
43
|
+
Lint/ConstantOverwrittenInRescue: # new in 1.31
|
44
|
+
Enabled: true
|
45
|
+
Lint/DeprecatedConstants: # new in 1.8
|
46
|
+
Enabled: true
|
47
|
+
Lint/DuplicateBranch: # new in 1.3
|
48
|
+
Enabled: true
|
49
|
+
Lint/DuplicateMagicComment: # new in 1.37
|
50
|
+
Enabled: true
|
51
|
+
Lint/DuplicateMatchPattern: # new in 1.50
|
52
|
+
Enabled: true
|
53
|
+
Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
|
54
|
+
Enabled: true
|
55
|
+
Lint/EmptyBlock: # new in 1.1
|
56
|
+
Enabled: true
|
57
|
+
Lint/EmptyClass: # new in 1.3
|
58
|
+
Enabled: true
|
59
|
+
Lint/EmptyInPattern: # new in 1.16
|
60
|
+
Enabled: true
|
61
|
+
Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
|
62
|
+
Enabled: true
|
63
|
+
Lint/ItWithoutArgumentsInBlock: # new in 1.59
|
64
|
+
Enabled: true
|
65
|
+
Lint/LambdaWithoutLiteralBlock: # new in 1.8
|
66
|
+
Enabled: true
|
67
|
+
Lint/LiteralAssignmentInCondition: # new in 1.58
|
68
|
+
Enabled: true
|
69
|
+
Lint/MixedCaseRange: # new in 1.53
|
70
|
+
Enabled: true
|
71
|
+
Lint/NoReturnInBeginEndBlocks: # new in 1.2
|
72
|
+
Enabled: true
|
73
|
+
Lint/NonAtomicFileOperation: # new in 1.31
|
74
|
+
Enabled: true
|
75
|
+
Lint/NumberedParameterAssignment: # new in 1.9
|
76
|
+
Enabled: true
|
77
|
+
Lint/OrAssignmentToConstant: # new in 1.9
|
78
|
+
Enabled: true
|
79
|
+
Lint/RedundantDirGlobSort: # new in 1.8
|
80
|
+
Enabled: true
|
81
|
+
Lint/RedundantRegexpQuantifiers: # new in 1.53
|
82
|
+
Enabled: true
|
83
|
+
Lint/RefinementImportMethods: # new in 1.27
|
84
|
+
Enabled: true
|
85
|
+
Lint/RequireRangeParentheses: # new in 1.32
|
86
|
+
Enabled: true
|
87
|
+
Lint/RequireRelativeSelfPath: # new in 1.22
|
88
|
+
Enabled: true
|
89
|
+
Lint/SymbolConversion: # new in 1.9
|
90
|
+
Enabled: true
|
91
|
+
Lint/ToEnumArguments: # new in 1.1
|
92
|
+
Enabled: true
|
93
|
+
Lint/TripleQuotes: # new in 1.9
|
94
|
+
Enabled: true
|
95
|
+
Lint/UnexpectedBlockArity: # new in 1.5
|
96
|
+
Enabled: true
|
97
|
+
Lint/UnmodifiedReduceAccumulator: # new in 1.1
|
98
|
+
Enabled: true
|
99
|
+
Lint/UselessRescue: # new in 1.43
|
100
|
+
Enabled: true
|
101
|
+
Lint/UselessRuby2Keywords: # new in 1.23
|
102
|
+
Enabled: true
|
103
|
+
Metrics/CollectionLiteralLength: # new in 1.47
|
104
|
+
Enabled: true
|
105
|
+
Naming/BlockForwarding: # new in 1.24
|
106
|
+
Enabled: true
|
107
|
+
Security/CompoundHash: # new in 1.28
|
108
|
+
Enabled: true
|
109
|
+
Security/IoMethods: # new in 1.22
|
110
|
+
Enabled: true
|
111
|
+
Style/ArgumentsForwarding: # new in 1.1
|
112
|
+
Enabled: true
|
113
|
+
Style/ArrayIntersect: # new in 1.40
|
114
|
+
Enabled: true
|
115
|
+
Style/CollectionCompact: # new in 1.2
|
116
|
+
Enabled: true
|
117
|
+
Style/ComparableClamp: # new in 1.44
|
118
|
+
Enabled: true
|
119
|
+
Style/ConcatArrayLiterals: # new in 1.41
|
120
|
+
Enabled: true
|
121
|
+
Style/DataInheritance: # new in 1.49
|
122
|
+
Enabled: true
|
123
|
+
Style/DirEmpty: # new in 1.48
|
124
|
+
Enabled: true
|
125
|
+
Style/DocumentDynamicEvalDefinition: # new in 1.1
|
126
|
+
Enabled: true
|
127
|
+
Style/EmptyHeredoc: # new in 1.32
|
128
|
+
Enabled: true
|
129
|
+
Style/EndlessMethod: # new in 1.8
|
130
|
+
Enabled: true
|
131
|
+
Style/EnvHome: # new in 1.29
|
132
|
+
Enabled: true
|
133
|
+
Style/ExactRegexpMatch: # new in 1.51
|
134
|
+
Enabled: true
|
135
|
+
Style/FetchEnvVar: # new in 1.28
|
136
|
+
Enabled: true
|
137
|
+
Style/FileEmpty: # new in 1.48
|
138
|
+
Enabled: true
|
139
|
+
Style/FileRead: # new in 1.24
|
140
|
+
Enabled: true
|
141
|
+
Style/FileWrite: # new in 1.24
|
142
|
+
Enabled: true
|
143
|
+
Style/HashConversion: # new in 1.10
|
144
|
+
Enabled: true
|
145
|
+
Style/HashExcept: # new in 1.7
|
146
|
+
Enabled: true
|
147
|
+
Style/IfWithBooleanLiteralBranches: # new in 1.9
|
148
|
+
Enabled: true
|
149
|
+
Style/InPatternThen: # new in 1.16
|
150
|
+
Enabled: true
|
151
|
+
Style/MagicCommentFormat: # new in 1.35
|
152
|
+
Enabled: true
|
153
|
+
Style/MapCompactWithConditionalBlock: # new in 1.30
|
154
|
+
Enabled: true
|
155
|
+
Style/MapToHash: # new in 1.24
|
156
|
+
Enabled: true
|
157
|
+
Style/MapToSet: # new in 1.42
|
158
|
+
Enabled: true
|
159
|
+
Style/MinMaxComparison: # new in 1.42
|
160
|
+
Enabled: true
|
161
|
+
Style/MultilineInPatternThen: # new in 1.16
|
162
|
+
Enabled: true
|
163
|
+
Style/NegatedIfElseCondition: # new in 1.2
|
164
|
+
Enabled: true
|
165
|
+
Style/NestedFileDirname: # new in 1.26
|
166
|
+
Enabled: true
|
167
|
+
Style/NilLambda: # new in 1.3
|
168
|
+
Enabled: true
|
169
|
+
Style/NumberedParameters: # new in 1.22
|
170
|
+
Enabled: true
|
171
|
+
Style/NumberedParametersLimit: # new in 1.22
|
172
|
+
Enabled: true
|
173
|
+
Style/ObjectThen: # new in 1.28
|
174
|
+
Enabled: true
|
175
|
+
Style/OpenStructUse: # new in 1.23
|
176
|
+
Enabled: true
|
177
|
+
Style/OperatorMethodCall: # new in 1.37
|
178
|
+
Enabled: true
|
179
|
+
Style/QuotedSymbols: # new in 1.16
|
180
|
+
Enabled: true
|
181
|
+
Style/RedundantArgument: # new in 1.4
|
182
|
+
Enabled: true
|
183
|
+
Style/RedundantArrayConstructor: # new in 1.52
|
184
|
+
Enabled: true
|
185
|
+
Style/RedundantConstantBase: # new in 1.40
|
186
|
+
Enabled: true
|
187
|
+
Style/RedundantCurrentDirectoryInPath: # new in 1.53
|
188
|
+
Enabled: true
|
189
|
+
Style/RedundantDoubleSplatHashBraces: # new in 1.41
|
190
|
+
Enabled: true
|
191
|
+
Style/RedundantEach: # new in 1.38
|
192
|
+
Enabled: true
|
193
|
+
Style/RedundantFilterChain: # new in 1.52
|
194
|
+
Enabled: true
|
195
|
+
Style/RedundantHeredocDelimiterQuotes: # new in 1.45
|
196
|
+
Enabled: true
|
197
|
+
Style/RedundantInitialize: # new in 1.27
|
198
|
+
Enabled: true
|
199
|
+
Style/RedundantLineContinuation: # new in 1.49
|
200
|
+
Enabled: true
|
201
|
+
Style/RedundantRegexpArgument: # new in 1.53
|
202
|
+
Enabled: true
|
203
|
+
Style/RedundantRegexpConstructor: # new in 1.52
|
204
|
+
Enabled: true
|
205
|
+
Style/RedundantSelfAssignmentBranch: # new in 1.19
|
206
|
+
Enabled: true
|
207
|
+
Style/RedundantStringEscape: # new in 1.37
|
208
|
+
Enabled: true
|
209
|
+
Style/ReturnNilInPredicateMethodDefinition: # new in 1.53
|
210
|
+
Enabled: true
|
211
|
+
Style/SelectByRegexp: # new in 1.22
|
212
|
+
Enabled: true
|
213
|
+
Style/SingleLineDoEndBlock: # new in 1.57
|
214
|
+
Enabled: true
|
215
|
+
Style/StringChars: # new in 1.12
|
216
|
+
Enabled: true
|
217
|
+
Style/SuperWithArgsParentheses: # new in 1.58
|
218
|
+
Enabled: true
|
219
|
+
Style/SwapValues: # new in 1.1
|
220
|
+
Enabled: true
|
221
|
+
Style/YAMLFileRead: # new in 1.53
|
222
|
+
Enabled: true
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# OCFL for Ruby
|
2
|
+
|
3
|
+
This is an implementation of the Oxford Common File Layout (OCFL) for Ruby. See https://ocfl.io for more information about OCFL.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
9
|
+
|
10
|
+
Install the gem and add to the application's Gemfile by executing:
|
11
|
+
|
12
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
13
|
+
|
14
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
15
|
+
|
16
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
directory = OCFL::Object::Directory.new(object_root: '/files/[object_root]')
|
22
|
+
directory.exists?
|
23
|
+
# => false
|
24
|
+
builder = OCFL::Object::DirectoryBuilder.new(object_root: 'spec/abc123', id: 'http://example.com/abc123')
|
25
|
+
builder.copy_file('sig/ocfl.rbs')
|
26
|
+
|
27
|
+
directory = builder.save
|
28
|
+
directory.exists?
|
29
|
+
# => true
|
30
|
+
directory.valid?
|
31
|
+
# => true
|
32
|
+
|
33
|
+
new_version = directory.begin_new_version
|
34
|
+
new_version.copy_file('sig/ocfl.rbs')
|
35
|
+
new_version.save
|
36
|
+
|
37
|
+
directory.head
|
38
|
+
# => 'v2'
|
39
|
+
|
40
|
+
directory.path("v2", "ocfl.rbs")
|
41
|
+
# => <Pathname:/files/[object_root]/v2/content/ocfl.rbs>
|
42
|
+
|
43
|
+
directory.path(:head, "ocfl.rbs")
|
44
|
+
# => <Pathname:/files/[object_root]/v2/content/ocfl.rbs>
|
45
|
+
```
|
46
|
+
|
47
|
+
## Development
|
48
|
+
|
49
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
50
|
+
|
51
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sul-dlss/ocfl.
|
data/Rakefile
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module OCFL
|
6
|
+
module Object
|
7
|
+
# An OCFL Directory layout for a particular object.
|
8
|
+
class Directory
|
9
|
+
# @param [String] object_root
|
10
|
+
# @param [Inventory] inventory this is only passed in when creating a new object. (see DirectoryBuilder)
|
11
|
+
def initialize(object_root:, inventory: nil)
|
12
|
+
@object_root = Pathname.new(object_root)
|
13
|
+
@version_inventory = {}
|
14
|
+
@version_inventory_errors = {}
|
15
|
+
@inventory = inventory
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :object_root, :errors
|
19
|
+
|
20
|
+
delegate :head, to: :inventory
|
21
|
+
|
22
|
+
def path(version, filename)
|
23
|
+
version = head if version == :head
|
24
|
+
relative_path = version_inventory(version).path(filename)
|
25
|
+
object_root + relative_path
|
26
|
+
end
|
27
|
+
|
28
|
+
def inventory
|
29
|
+
@inventory ||= begin
|
30
|
+
data = InventoryLoader.load(object_root + "inventory.json")
|
31
|
+
if data.success?
|
32
|
+
Inventory.new(data: data.value!)
|
33
|
+
else
|
34
|
+
@errors = data.failure
|
35
|
+
puts @errors.messages.inspect
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def head_inventory
|
42
|
+
version_inventory(inventory.head)
|
43
|
+
end
|
44
|
+
|
45
|
+
def version_inventory(version)
|
46
|
+
@version_inventory[version] ||= begin
|
47
|
+
data = InventoryLoader.load(object_root + version + "inventory.json")
|
48
|
+
if data.success?
|
49
|
+
Inventory.new(data: data.value!)
|
50
|
+
else
|
51
|
+
@version_inventory_errors[version] = data.failure
|
52
|
+
puts @version_inventory_errors[version].messages.inspect
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def reload
|
59
|
+
@version_inventory = {}
|
60
|
+
@inventory = nil
|
61
|
+
@errors = nil
|
62
|
+
@version_inventory_errors = {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def begin_new_version
|
66
|
+
DraftVersion.new(object_directory: self)
|
67
|
+
end
|
68
|
+
|
69
|
+
def exists?
|
70
|
+
namaste_exists?
|
71
|
+
end
|
72
|
+
|
73
|
+
def valid?
|
74
|
+
InventoryValidator.new(directory: object_root).valid? &&
|
75
|
+
namaste_exists? &&
|
76
|
+
!inventory.nil? && # Ensures it could be loaded
|
77
|
+
head_directory_valid?
|
78
|
+
end
|
79
|
+
|
80
|
+
def head_directory_valid?
|
81
|
+
InventoryValidator.new(directory: object_root + inventory.head).valid? &&
|
82
|
+
!head_inventory.nil? # Ensures it could be loaded
|
83
|
+
end
|
84
|
+
|
85
|
+
def namaste_exists?
|
86
|
+
File.exist?(namaste_file)
|
87
|
+
end
|
88
|
+
|
89
|
+
def namaste_file
|
90
|
+
object_root + "0=ocfl_object_1.1"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# rubocop:enable Style/StringConcatenation
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module OCFL
|
6
|
+
module Object
|
7
|
+
# Creates a OCFL Directory layout for a particular object.
|
8
|
+
class DirectoryBuilder
|
9
|
+
class ObjectExists < Error; end
|
10
|
+
|
11
|
+
def initialize(object_root:, id:)
|
12
|
+
@object_root = Pathname.new(object_root)
|
13
|
+
raise ObjectExists, "The directory `#{object_root}' already exists" if @object_root.exist?
|
14
|
+
|
15
|
+
@id = id
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :id, :object_root, :object_directory
|
19
|
+
|
20
|
+
def copy_file(...)
|
21
|
+
create_directory
|
22
|
+
version.copy_file(...)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_directory
|
26
|
+
FileUtils.mkdir_p(object_root)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Directory]
|
30
|
+
def save
|
31
|
+
version_path = object_root + "v1"
|
32
|
+
FileUtils.mkdir_p(version_path) unless version_path.exist? # in case no files were added
|
33
|
+
|
34
|
+
FileUtils.touch(object_root + "0=ocfl_object_1.1")
|
35
|
+
write_inventory
|
36
|
+
object_directory
|
37
|
+
end
|
38
|
+
|
39
|
+
def version
|
40
|
+
@version ||= begin
|
41
|
+
data = Inventory::InventoryStruct.new(id:, version: "v0", type: Inventory::URI_1_1, digestAlgorithm: "sha512",
|
42
|
+
head: "v0", versions: {}, manifest: {})
|
43
|
+
inventory = Inventory.new(data:)
|
44
|
+
@object_directory = Directory.new(object_root: @object_root, inventory:)
|
45
|
+
DraftVersion.new(object_directory:)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_inventory
|
50
|
+
version.save
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# rubocop:enable Style/StringConcatenation
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module OCFL
|
6
|
+
module Object
|
7
|
+
# A new OCFL version
|
8
|
+
class DraftVersion
|
9
|
+
# @params [Directory] object_directory
|
10
|
+
def initialize(object_directory:)
|
11
|
+
@object_directory = object_directory
|
12
|
+
@manifest = object_directory.inventory.manifest.dup
|
13
|
+
@state = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :object_directory, :manifest, :state
|
17
|
+
|
18
|
+
def move_file(incoming_path)
|
19
|
+
prepare_content_directory
|
20
|
+
add(incoming_path)
|
21
|
+
FileUtils.mv(incoming_path, content_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy_file(incoming_path)
|
25
|
+
prepare_content_directory
|
26
|
+
add(incoming_path)
|
27
|
+
FileUtils.cp(incoming_path, content_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
# def copy_directory(incoming_path)
|
31
|
+
# prepare_content_directory
|
32
|
+
# Dir.foreach(incoming_path) do |file_name|
|
33
|
+
# next if ['.', '..'].include?(file_name)
|
34
|
+
# add(incoming_path)
|
35
|
+
# FileUtils.cp(incoming_path, content_path)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
|
39
|
+
def add(incoming_path)
|
40
|
+
digest = Digest::SHA512.file(incoming_path).to_s
|
41
|
+
version_content_path = content_path.relative_path_from(object_directory.object_root)
|
42
|
+
logical_file_path = File.basename(incoming_path)
|
43
|
+
file_path_relative_to_root = (version_content_path + logical_file_path).to_s
|
44
|
+
@manifest[digest] = [file_path_relative_to_root]
|
45
|
+
@state[digest] = [logical_file_path]
|
46
|
+
end
|
47
|
+
|
48
|
+
def prepare_content_directory
|
49
|
+
prepare_directory
|
50
|
+
return if @prepared_content
|
51
|
+
|
52
|
+
FileUtils.mkdir(content_path)
|
53
|
+
@prepared_content = true
|
54
|
+
end
|
55
|
+
|
56
|
+
def prepare_directory
|
57
|
+
return if @prepared
|
58
|
+
|
59
|
+
FileUtils.mkdir(path)
|
60
|
+
@prepared = true
|
61
|
+
end
|
62
|
+
|
63
|
+
def content_path
|
64
|
+
path + "content"
|
65
|
+
end
|
66
|
+
|
67
|
+
def path
|
68
|
+
object_directory.object_root + version_number
|
69
|
+
end
|
70
|
+
|
71
|
+
def version_number
|
72
|
+
@version_number ||= "v#{object_directory.head.delete_prefix("v").to_i + 1}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_inventory
|
76
|
+
old_data = object_directory.inventory.data
|
77
|
+
versions = old_data.versions.merge(version_number => Version.new(created: Time.now.utc.iso8601, state: @state))
|
78
|
+
Inventory::InventoryStruct.new(old_data.to_h.merge(manifest:, head: version_number, versions:))
|
79
|
+
end
|
80
|
+
|
81
|
+
def save
|
82
|
+
inventory = build_inventory
|
83
|
+
InventoryWriter.new(inventory:, path:).write
|
84
|
+
FileUtils.cp(path + "inventory.json", object_directory.object_root)
|
85
|
+
FileUtils.cp(path + "inventory.json.sha512", object_directory.object_root)
|
86
|
+
object_directory.reload
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OCFL
|
4
|
+
module Object
|
5
|
+
# Represents the JSON file that stores the object inventory
|
6
|
+
# https://ocfl.io/1.1/spec/#inventory
|
7
|
+
class Inventory
|
8
|
+
URI_1_1 = "https://ocfl.io/1.1/spec/#inventory"
|
9
|
+
|
10
|
+
# A data structure for the inventory
|
11
|
+
class InventoryStruct < Dry::Struct
|
12
|
+
transform_keys(&:to_sym)
|
13
|
+
attribute :id, Types::String
|
14
|
+
attribute :type, Types::String
|
15
|
+
attribute :digestAlgorithm, Types::String
|
16
|
+
attribute :head, Types::String
|
17
|
+
attribute? :contentDirectory, Types::String
|
18
|
+
attribute :versions, Types::Hash.map(Types::String, Version)
|
19
|
+
attribute :manifest, Types::Hash
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(data:)
|
23
|
+
@data = data
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :errors, :data
|
27
|
+
|
28
|
+
delegate :id, :head, :versions, :manifest, to: :data
|
29
|
+
|
30
|
+
def content_directory
|
31
|
+
data.contentDirectory || "content"
|
32
|
+
end
|
33
|
+
|
34
|
+
# @returns [String] the path to the file relative to the object root. (e.g. v2/content/image.tiff)
|
35
|
+
def path(logical_path)
|
36
|
+
sha = versions[head].state.find { |_sha, file_names| file_names.include?(logical_path) }&.first
|
37
|
+
|
38
|
+
return unless sha
|
39
|
+
|
40
|
+
manifest.fetch(sha).first
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OCFL
|
4
|
+
module Object
|
5
|
+
# Loads and Inventory object from JSON
|
6
|
+
class InventoryLoader
|
7
|
+
include Dry::Monads[:result]
|
8
|
+
|
9
|
+
VersionEnum = Types::String.enum(Inventory::URI_1_1)
|
10
|
+
DigestAlgorithm = Types::String.enum("md5", "sha1", "sha256", "sha512", "blake2b-512")
|
11
|
+
|
12
|
+
# https://ocfl.io/1.1/spec/#inventory-structure
|
13
|
+
# Validation of the incoming data
|
14
|
+
Schema = Dry::Schema.Params do
|
15
|
+
# config.validate_keys = true
|
16
|
+
required(:id).filled(:string)
|
17
|
+
required(:type).filled(VersionEnum)
|
18
|
+
required(:digestAlgorithm).filled(DigestAlgorithm)
|
19
|
+
required(:head).filled(:string)
|
20
|
+
optional(:contentDirectory).filled(:string)
|
21
|
+
required(:versions).hash
|
22
|
+
required(:manifest).hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load(file_name)
|
26
|
+
new(file_name).load
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(file_name)
|
30
|
+
@file_name = file_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def load
|
34
|
+
bytestream = File.read(@file_name)
|
35
|
+
data = JSON.parse(bytestream)
|
36
|
+
errors = Schema.call(data).errors
|
37
|
+
if errors.empty?
|
38
|
+
Success(Inventory::InventoryStruct.new(data))
|
39
|
+
else
|
40
|
+
Failure(errors)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module OCFL
|
6
|
+
module Object
|
7
|
+
# Checks to see that the inventory.json and it's checksum in a direcotory are valid
|
8
|
+
class InventoryValidator
|
9
|
+
def initialize(directory:)
|
10
|
+
@directory = Pathname.new(directory)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :directory
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
inventory_file_exists? && inventory_file_matches_checksum?
|
17
|
+
end
|
18
|
+
|
19
|
+
def inventory_file_exists?
|
20
|
+
File.exist?(inventory_file)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inventory_file_matches_checksum?
|
24
|
+
return false unless File.exist?(inventory_checksum_file)
|
25
|
+
|
26
|
+
actual = inventory_file_checksum
|
27
|
+
expected = File.read(inventory_checksum_file)
|
28
|
+
expected.match?(/\A#{actual}\s+inventory\.json\z/)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inventory_checksum_file
|
32
|
+
directory + "inventory.json.sha512"
|
33
|
+
end
|
34
|
+
|
35
|
+
def inventory_file_checksum
|
36
|
+
Digest::SHA512.file inventory_file
|
37
|
+
end
|
38
|
+
|
39
|
+
def inventory_file
|
40
|
+
directory + "inventory.json"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
# rubocop:enable Style/StringConcatenation
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module OCFL
|
6
|
+
module Object
|
7
|
+
# Writes a OCFL Inventory to json on disk
|
8
|
+
class InventoryWriter
|
9
|
+
def initialize(inventory:, path:)
|
10
|
+
@path = path
|
11
|
+
@inventory = inventory
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :inventory, :path
|
15
|
+
|
16
|
+
def write
|
17
|
+
write_inventory
|
18
|
+
update_inventory_checksum
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_inventory
|
22
|
+
File.write(inventory_file, JSON.pretty_generate(inventory.to_h))
|
23
|
+
end
|
24
|
+
|
25
|
+
def inventory_file
|
26
|
+
path + "inventory.json"
|
27
|
+
end
|
28
|
+
|
29
|
+
def checksum_file
|
30
|
+
path + "inventory.json.sha512"
|
31
|
+
end
|
32
|
+
|
33
|
+
def update_inventory_checksum
|
34
|
+
digest = Digest::SHA512.file inventory_file
|
35
|
+
File.write(checksum_file, "#{digest} inventory.json")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# rubocop:enable Style/StringConcatenation
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OCFL
|
4
|
+
module Object
|
5
|
+
# Represents the OCFL version
|
6
|
+
# https://ocfl.io/1.1/spec/#version
|
7
|
+
class Version < Dry.Struct
|
8
|
+
# Represents the OCFL user
|
9
|
+
class User < Dry.Struct
|
10
|
+
transform_keys(&:to_sym)
|
11
|
+
attribute :name, Types::String
|
12
|
+
attribute? :address, Types::String
|
13
|
+
end
|
14
|
+
|
15
|
+
transform_keys(&:to_sym)
|
16
|
+
attribute :created, Types::String
|
17
|
+
attribute :state, Types::Hash.map(Types::String, Types::Array.of(Types::String))
|
18
|
+
attribute? :message, Types::String
|
19
|
+
attribute? :user, User
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/ocfl/object.rb
ADDED
data/lib/ocfl/version.rb
ADDED
data/lib/ocfl.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ocfl/version"
|
4
|
+
require "zeitwerk"
|
5
|
+
require "json"
|
6
|
+
require "dry/monads"
|
7
|
+
require "dry-schema"
|
8
|
+
require "dry-struct"
|
9
|
+
require "active_support"
|
10
|
+
require "active_support/core_ext/module/delegation"
|
11
|
+
|
12
|
+
# Custom zeitwerk inflector for OCFL
|
13
|
+
class OCFLInflector < Zeitwerk::Inflector
|
14
|
+
def camelize(basename, _abspath)
|
15
|
+
return "OCFL" if basename == "ocfl"
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
loader = Zeitwerk::Loader.for_gem
|
22
|
+
loader.inflector = OCFLInflector.new
|
23
|
+
loader.setup
|
24
|
+
|
25
|
+
module OCFL
|
26
|
+
class Error < StandardError; end
|
27
|
+
|
28
|
+
# Schema types
|
29
|
+
module Types
|
30
|
+
include Dry.Types()
|
31
|
+
end
|
32
|
+
end
|
data/sig/ocfl.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ocfl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Coyne
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-monads
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-schema
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dry-struct
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: zeitwerk
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
description: See https://ocfl.io/
|
84
|
+
email:
|
85
|
+
- jcoyne@justincoyne.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".rspec"
|
91
|
+
- ".rubocop.yml"
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- lib/ocfl.rb
|
95
|
+
- lib/ocfl/object.rb
|
96
|
+
- lib/ocfl/object/directory.rb
|
97
|
+
- lib/ocfl/object/directory_builder.rb
|
98
|
+
- lib/ocfl/object/draft_version.rb
|
99
|
+
- lib/ocfl/object/inventory.rb
|
100
|
+
- lib/ocfl/object/inventory_loader.rb
|
101
|
+
- lib/ocfl/object/inventory_validator.rb
|
102
|
+
- lib/ocfl/object/inventory_writer.rb
|
103
|
+
- lib/ocfl/object/version.rb
|
104
|
+
- lib/ocfl/version.rb
|
105
|
+
- sig/ocfl.rbs
|
106
|
+
homepage: https://github.com/sul-dlss/ocfl-rb
|
107
|
+
licenses: []
|
108
|
+
metadata:
|
109
|
+
homepage_uri: https://github.com/sul-dlss/ocfl-rb
|
110
|
+
source_code_uri: https://github.com/sul-dlss/ocfl-rb
|
111
|
+
changelog_uri: https://github.com/sul-dlss/ocfl-rb/releases
|
112
|
+
rubygems_mfa_required: 'true'
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 3.1.0
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.5.4
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: A ruby library for interacting with the Oxford Common File Layout (OCFL)
|
132
|
+
test_files: []
|