ocfl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OCFL
4
+ # An OCFL Object is a group of one or more content files and administrative information
5
+ # https://ocfl.io/1.1/spec/#object-spec
6
+ module Object; end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OCFL
4
+ VERSION = "0.1.0"
5
+ end
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
@@ -0,0 +1,4 @@
1
+ module Ocfl
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []