ocfl 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/README.md +33 -20
- data/lib/ocfl/inventory.rb +47 -0
- data/lib/ocfl/inventory_loader.rb +43 -0
- data/lib/ocfl/inventory_validator.rb +40 -0
- data/lib/ocfl/inventory_writer.rb +35 -0
- data/lib/ocfl/layouts/druid_tree.rb +21 -0
- data/lib/ocfl/object.rb +137 -1
- data/lib/ocfl/object_version.rb +24 -0
- data/lib/ocfl/storage_root.rb +46 -0
- data/lib/ocfl/version.rb +1 -1
- data/lib/ocfl/version_builder.rb +152 -0
- data/tmp/.keep +0 -0
- metadata +11 -10
- data/lib/ocfl/object/directory.rb +0 -107
- data/lib/ocfl/object/directory_builder.rb +0 -69
- data/lib/ocfl/object/draft_version.rb +0 -143
- data/lib/ocfl/object/inventory.rb +0 -49
- data/lib/ocfl/object/inventory_loader.rb +0 -45
- data/lib/ocfl/object/inventory_validator.rb +0 -42
- data/lib/ocfl/object/inventory_writer.rb +0 -37
- data/lib/ocfl/object/version.rb +0 -26
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8720d3032a5759d6cea1547b752534735d2a080fa69f3065b44a00ac7ace2dd7
         | 
| 4 | 
            +
              data.tar.gz: d60f0d3e6f31a6b5d1085750e87dde586cda0f7cc2ce88447d461726b83179cb
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 29bcba42064fb03f004ab9706378db6dfd7eae53d6b730f986863e79d0c658034c64e5582dd989cd0b0cc4275badc657b3b9113f9e48080188db63669c51df2c
         | 
| 7 | 
            +
              data.tar.gz: 577e796d83264f7566a3369c8e07cb88750b98314d018e2f394fe8ed63aad0fa275c04dd4b53ac4ab6e96449e1056718b26052fe7680ebe64954d3425bf54276
         | 
    
        data/.rubocop.yml
    CHANGED
    
    | @@ -22,12 +22,19 @@ Metrics/BlockLength: | |
| 22 22 | 
             
                - describe
         | 
| 23 23 | 
             
                - context
         | 
| 24 24 |  | 
| 25 | 
            +
            Metrics/ClassLength:
         | 
| 26 | 
            +
              Max: 110
         | 
| 27 | 
            +
             | 
| 25 28 | 
             
            RSpec/MultipleExpectations:
         | 
| 26 29 | 
             
              Enabled: false
         | 
| 27 30 |  | 
| 28 31 | 
             
            RSpec/ExampleLength:
         | 
| 29 32 | 
             
              Max: 10
         | 
| 30 33 |  | 
| 34 | 
            +
            RSpec/InstanceVariable:
         | 
| 35 | 
            +
              Exclude:
         | 
| 36 | 
            +
                - spec/support/temp_directory.rb
         | 
| 37 | 
            +
             | 
| 31 38 | 
             
            RSpec/MultipleMemoizedHelpers:
         | 
| 32 39 | 
             
              Max: 8
         | 
| 33 40 |  | 
    
        data/README.md
    CHANGED
    
    | @@ -16,36 +16,50 @@ If bundler is not being used to manage dependencies, install the gem by executin | |
| 16 16 | 
             
            ## Usage
         | 
| 17 17 |  | 
| 18 18 | 
             
            ```ruby
         | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 19 | 
            +
            storage_root = OCFL::StorageRoot.new(base_directory: '/files')
         | 
| 20 | 
            +
            storage_root.exists?
         | 
| 21 | 
            +
            # => false
         | 
| 22 | 
            +
            storage_root.valid?
         | 
| 21 23 | 
             
            # => false
         | 
| 22 | 
            -
            builder = OCFL::Object::DirectoryBuilder.new(object_root: 'spec/abc123', id: 'http://example.com/abc123')
         | 
| 23 | 
            -
            builder.copy_file('sig/ocfl.rbs', destination_path: 'ocfl/types/generated.rbs')
         | 
| 24 24 |  | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 25 | 
            +
            storage_root.save
         | 
| 26 | 
            +
            storage_root.exists?
         | 
| 27 27 | 
             
            # => true
         | 
| 28 | 
            -
             | 
| 28 | 
            +
            storage_root.valid?
         | 
| 29 29 | 
             
            # => true
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            object = storage_root.object('bc123df4567') # returns an instance of `OCFL::Object`
         | 
| 32 | 
            +
            object.exists?
         | 
| 33 | 
            +
            # => false
         | 
| 34 | 
            +
            object.valid?
         | 
| 35 | 
            +
            # => false
         | 
| 36 | 
            +
            object.head
         | 
| 37 | 
            +
            # => 'v0'
         | 
| 30 38 | 
             
            ```
         | 
| 31 39 |  | 
| 32 40 | 
             
            ### Versions
         | 
| 33 41 |  | 
| 34 | 
            -
             | 
| 42 | 
            +
            To build out an object, you'll need to create one or more versions.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            There are three ways to get a version within an existing object directory.
         | 
| 35 45 |  | 
| 36 46 | 
             
            #### Start a new version
         | 
| 37 47 | 
             
            ```
         | 
| 38 | 
            -
            new_version =  | 
| 39 | 
            -
            new_version.copy_file('sig/ocfl.rbs')
         | 
| 48 | 
            +
            new_version = object.begin_new_version
         | 
| 49 | 
            +
            new_version.copy_file('sig/ocfl.rbs', destination_path: 'ocfl/types/generated.rbs')
         | 
| 40 50 | 
             
            new_version.save
         | 
| 41 51 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
            # =>  | 
| 52 | 
            +
            object.exists?
         | 
| 53 | 
            +
            # => true
         | 
| 54 | 
            +
            object.valid?
         | 
| 55 | 
            +
            # => true
         | 
| 56 | 
            +
            object.head
         | 
| 57 | 
            +
            # => 'v1'
         | 
| 44 58 | 
             
            ```
         | 
| 45 59 |  | 
| 46 60 | 
             
            #### Modify the existing head version
         | 
| 47 61 | 
             
            ```
         | 
| 48 | 
            -
            new_version =  | 
| 62 | 
            +
            new_version = object.head_version
         | 
| 49 63 | 
             
            new_version.delete_file('sample.txt')
         | 
| 50 64 | 
             
            new_version.copy_file('sig/ocfl.rbs')
         | 
| 51 65 | 
             
            new_version.save
         | 
| @@ -53,7 +67,7 @@ new_version.save | |
| 53 67 |  | 
| 54 68 | 
             
            #### Overwrite the existing head version
         | 
| 55 69 | 
             
            ```
         | 
| 56 | 
            -
            new_version =  | 
| 70 | 
            +
            new_version = object.overwrite_current_version
         | 
| 57 71 | 
             
            new_version.copy_file('sig/ocfl.rbs')
         | 
| 58 72 | 
             
            new_version.save
         | 
| 59 73 | 
             
            ```
         | 
| @@ -61,7 +75,7 @@ new_version.save | |
| 61 75 | 
             
            ### File paths
         | 
| 62 76 | 
             
            ```
         | 
| 63 77 | 
             
            # List file names that were part of a given version
         | 
| 64 | 
            -
             | 
| 78 | 
            +
            object.versions['v1'].file_names
         | 
| 65 79 | 
             
            # => ["ocfl.rbs"]
         | 
| 66 80 |  | 
| 67 81 | 
             
            # Or on the head version
         | 
| @@ -69,13 +83,12 @@ directory.head_version.file_names | |
| 69 83 | 
             
            # => ["ocfl.rbs"]
         | 
| 70 84 |  | 
| 71 85 | 
             
            # Get the path of a file in a given version
         | 
| 72 | 
            -
             | 
| 73 | 
            -
            # => <Pathname:/files/[object_root]/ | 
| 86 | 
            +
            object.path(filepath: "ocfl.rbs", version: "v1")
         | 
| 87 | 
            +
            # => <Pathname:/files/[object_root]/v1/content/ocfl.rbs>
         | 
| 74 88 |  | 
| 75 89 | 
             
            # Get the path of a file in the head version
         | 
| 76 | 
            -
             | 
| 77 | 
            -
            # => <Pathname:/files/[object_root]/ | 
| 78 | 
            -
             | 
| 90 | 
            +
            object.path(filepath: "ocfl.rbs")
         | 
| 91 | 
            +
            # => <Pathname:/files/[object_root]/v1/content/ocfl.rbs>
         | 
| 79 92 | 
             
            ```
         | 
| 80 93 |  | 
| 81 94 | 
             
            ## Development
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Represents the JSON file that stores the object inventory
         | 
| 5 | 
            +
              # https://ocfl.io/1.1/spec/#inventory
         | 
| 6 | 
            +
              class Inventory
         | 
| 7 | 
            +
                URI_1_1 = "https://ocfl.io/1.1/spec/#inventory"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                # A data structure for the inventory
         | 
| 10 | 
            +
                class InventoryStruct < Dry::Struct
         | 
| 11 | 
            +
                  transform_keys(&:to_sym)
         | 
| 12 | 
            +
                  attribute :id, Types::String
         | 
| 13 | 
            +
                  attribute :type, Types::String
         | 
| 14 | 
            +
                  attribute :digestAlgorithm, Types::String
         | 
| 15 | 
            +
                  attribute :head, Types::String
         | 
| 16 | 
            +
                  attribute? :contentDirectory, Types::String
         | 
| 17 | 
            +
                  attribute :versions, Types::Hash.map(Types::String, ObjectVersion)
         | 
| 18 | 
            +
                  attribute :manifest, Types::Hash
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize(data:)
         | 
| 22 | 
            +
                  @data = data
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                attr_reader :errors, :data
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                delegate :id, :head, :versions, :manifest, to: :data
         | 
| 28 | 
            +
                delegate :state, to: :head_version
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def content_directory
         | 
| 31 | 
            +
                  data.contentDirectory || "content"
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # @return [String,nil] the path to the file relative to the object root. (e.g. v2/content/image.tiff)
         | 
| 35 | 
            +
                def path(logical_path)
         | 
| 36 | 
            +
                  digest, = state.find { |_, logical_paths| logical_paths.include?(logical_path) }
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  return unless digest
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  manifest[digest].find { |content_path| content_path.match(%r{\Av\d+/#{content_directory}/#{logical_path}\z}) }
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def head_version
         | 
| 44 | 
            +
                  versions[head]
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Loads and Inventory object from JSON
         | 
| 5 | 
            +
              class InventoryLoader
         | 
| 6 | 
            +
                include Dry::Monads[:result]
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                VersionEnum = Types::String.enum(Inventory::URI_1_1)
         | 
| 9 | 
            +
                DigestAlgorithm = Types::String.enum("md5", "sha1", "sha256", "sha512", "blake2b-512")
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # https://ocfl.io/1.1/spec/#inventory-structure
         | 
| 12 | 
            +
                # Validation of the incoming data
         | 
| 13 | 
            +
                Schema = Dry::Schema.Params do
         | 
| 14 | 
            +
                  # config.validate_keys = true
         | 
| 15 | 
            +
                  required(:id).filled(:string)
         | 
| 16 | 
            +
                  required(:type).filled(VersionEnum)
         | 
| 17 | 
            +
                  required(:digestAlgorithm).filled(DigestAlgorithm)
         | 
| 18 | 
            +
                  required(:head).filled(:string)
         | 
| 19 | 
            +
                  optional(:contentDirectory).filled(:string)
         | 
| 20 | 
            +
                  required(:versions).hash
         | 
| 21 | 
            +
                  required(:manifest).hash
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def self.load(file_name)
         | 
| 25 | 
            +
                  new(file_name).load
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def initialize(file_name)
         | 
| 29 | 
            +
                  @file_name = file_name
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def load
         | 
| 33 | 
            +
                  bytestream = File.read(@file_name)
         | 
| 34 | 
            +
                  data = JSON.parse(bytestream)
         | 
| 35 | 
            +
                  errors = Schema.call(data).errors
         | 
| 36 | 
            +
                  if errors.empty?
         | 
| 37 | 
            +
                    Success(Inventory::InventoryStruct.new(data))
         | 
| 38 | 
            +
                  else
         | 
| 39 | 
            +
                    Failure(errors)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Checks to see that the inventory.json and it's checksum in a direcotory are valid
         | 
| 5 | 
            +
              class InventoryValidator
         | 
| 6 | 
            +
                def initialize(directory:)
         | 
| 7 | 
            +
                  @directory = Pathname.new(directory)
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                attr_reader :directory
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def valid?
         | 
| 13 | 
            +
                  inventory_file_exists? && inventory_file_matches_checksum?
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def inventory_file_exists?
         | 
| 17 | 
            +
                  File.exist?(inventory_file)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def inventory_file_matches_checksum?
         | 
| 21 | 
            +
                  return false unless File.exist?(inventory_checksum_file)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  actual = inventory_file_checksum
         | 
| 24 | 
            +
                  expected = File.read(inventory_checksum_file)
         | 
| 25 | 
            +
                  expected.match?(/\A#{actual}\s+inventory\.json\z/)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def inventory_checksum_file
         | 
| 29 | 
            +
                  directory / "inventory.json.sha512"
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def inventory_file_checksum
         | 
| 33 | 
            +
                  Digest::SHA512.file inventory_file
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def inventory_file
         | 
| 37 | 
            +
                  directory / "inventory.json"
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Writes a OCFL Inventory to json on disk
         | 
| 5 | 
            +
              class InventoryWriter
         | 
| 6 | 
            +
                def initialize(inventory:, path:)
         | 
| 7 | 
            +
                  @path = path
         | 
| 8 | 
            +
                  @inventory = inventory
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                attr_reader :inventory, :path
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def write
         | 
| 14 | 
            +
                  write_inventory
         | 
| 15 | 
            +
                  update_inventory_checksum
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def write_inventory
         | 
| 19 | 
            +
                  File.write(inventory_file, JSON.pretty_generate(inventory.to_h))
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def inventory_file
         | 
| 23 | 
            +
                  path / "inventory.json"
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def checksum_file
         | 
| 27 | 
            +
                  path / "inventory.json.sha512"
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def update_inventory_checksum
         | 
| 31 | 
            +
                  digest = Digest::SHA512.file inventory_file
         | 
| 32 | 
            +
                  File.write(checksum_file, "#{digest} inventory.json")
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              module Layouts
         | 
| 5 | 
            +
                # An OCFL Storage Root layout for the druid-tree structure
         | 
| 6 | 
            +
                # @see https://ocfl.io/1.1/spec/#root-structure
         | 
| 7 | 
            +
                class DruidTree
         | 
| 8 | 
            +
                  DRUID_PARTS_PATTERN = /\A([b-df-hjkmnp-tv-z]{2})([0-9]{3})([b-df-hjkmnp-tv-z]{2})([0-9]{4})\z/i
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.path_to(identifier)
         | 
| 11 | 
            +
                    segments = Array(identifier&.match(DRUID_PARTS_PATTERN)&.captures)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    raise "druid '#{identifier}' is invalid" unless segments.count == 4
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    Pathname.new(
         | 
| 16 | 
            +
                      File.join(segments)
         | 
| 17 | 
            +
                    )
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
    
        data/lib/ocfl/object.rb
    CHANGED
    
    | @@ -3,7 +3,143 @@ | |
| 3 3 | 
             
            module OCFL
         | 
| 4 4 | 
             
              # An OCFL Object is a group of one or more content files and administrative information
         | 
| 5 5 | 
             
              # https://ocfl.io/1.1/spec/#object-spec
         | 
| 6 | 
            -
               | 
| 6 | 
            +
              class Object
         | 
| 7 7 | 
             
                class FileNotFound < RuntimeError; end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                # @param [String] identifier an object identifier
         | 
| 10 | 
            +
                # @param [Pathname, String] root the path to the object root within the OCFL structure
         | 
| 11 | 
            +
                # @param [Inventory, nil] inventory this is only passed in when creating a new version
         | 
| 12 | 
            +
                # @param [String, nil] content_directory the directory to store versions in
         | 
| 13 | 
            +
                def initialize(root:, identifier:, inventory: nil, content_directory: nil)
         | 
| 14 | 
            +
                  @identifier = identifier
         | 
| 15 | 
            +
                  @root = Pathname.new(root)
         | 
| 16 | 
            +
                  @content_directory = content_directory
         | 
| 17 | 
            +
                  @version_inventory = {}
         | 
| 18 | 
            +
                  @version_inventory_errors = {}
         | 
| 19 | 
            +
                  @inventory = inventory
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                attr_reader :root, :errors, :identifier
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                delegate :head, :versions, :manifest, to: :inventory
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def exists?
         | 
| 27 | 
            +
                  namaste_file.exist?
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def path(filepath:, version: nil)
         | 
| 31 | 
            +
                  version ||= head
         | 
| 32 | 
            +
                  relative_path = version_inventory(version).path(filepath)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  raise FileNotFound, "Path '#{filepath}' not found in #{version} inventory" if relative_path.nil?
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  root / relative_path
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def inventory
         | 
| 40 | 
            +
                  @inventory ||= begin
         | 
| 41 | 
            +
                    maybe_inventory, inventory_loading_errors = load_or_initialize_inventory
         | 
| 42 | 
            +
                    if maybe_inventory
         | 
| 43 | 
            +
                      maybe_inventory
         | 
| 44 | 
            +
                    else
         | 
| 45 | 
            +
                      @errors = inventory_loading_errors
         | 
| 46 | 
            +
                      puts @errors.messages.inspect
         | 
| 47 | 
            +
                      nil
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def head_inventory
         | 
| 53 | 
            +
                  version_inventory(inventory.head)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def version_inventory(version)
         | 
| 57 | 
            +
                  @version_inventory[version] ||= begin
         | 
| 58 | 
            +
                    maybe_inventory, inventory_loading_errors = load_or_initialize_inventory(version:)
         | 
| 59 | 
            +
                    if maybe_inventory
         | 
| 60 | 
            +
                      maybe_inventory
         | 
| 61 | 
            +
                    else
         | 
| 62 | 
            +
                      @version_inventory_errors[version] = inventory_loading_errors
         | 
| 63 | 
            +
                      puts @version_inventory_errors[version].messages.inspect
         | 
| 64 | 
            +
                      nil
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def valid?
         | 
| 70 | 
            +
                  InventoryValidator.new(directory: root).valid? &&
         | 
| 71 | 
            +
                    exists? &&
         | 
| 72 | 
            +
                    !inventory.nil? && # Ensures it could be loaded
         | 
| 73 | 
            +
                    head_directory_valid?
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def head_directory_valid?
         | 
| 77 | 
            +
                  InventoryValidator.new(directory: root / inventory.head).valid? &&
         | 
| 78 | 
            +
                    !head_inventory.nil? # Ensures it could be loaded
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                # Start a completely new version
         | 
| 82 | 
            +
                def begin_new_version
         | 
| 83 | 
            +
                  VersionBuilder.new(object: self, state:)
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # Get a handle for the head version
         | 
| 87 | 
            +
                def head_version
         | 
| 88 | 
            +
                  VersionBuilder.new(object: self, overwrite_head: true, state: head_inventory.state)
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                # Get a handle that will replace the existing head version
         | 
| 92 | 
            +
                def overwrite_current_version
         | 
| 93 | 
            +
                  VersionBuilder.new(object: self, overwrite_head: true)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def reload
         | 
| 97 | 
            +
                  @version_inventory = {}
         | 
| 98 | 
            +
                  @inventory = nil
         | 
| 99 | 
            +
                  @errors = nil
         | 
| 100 | 
            +
                  @version_inventory_errors = {}
         | 
| 101 | 
            +
                  true
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def namaste_file
         | 
| 105 | 
            +
                  root / "0=ocfl_object_1.1"
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                private
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                def load_or_initialize_inventory(version: "")
         | 
| 111 | 
            +
                  inventory_path = root / version / "inventory.json"
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  return [new_inventory, nil] unless inventory_path.exist?
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  data = InventoryLoader.load(inventory_path)
         | 
| 116 | 
            +
                  if data.success?
         | 
| 117 | 
            +
                    [Inventory.new(data: data.value!), nil]
         | 
| 118 | 
            +
                  else
         | 
| 119 | 
            +
                    [nil, data.failure]
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def state
         | 
| 124 | 
            +
                  return {} if inventory.head == "v0"
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  head_inventory.state
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def new_inventory # rubocop:disable Metrics/MethodLength
         | 
| 130 | 
            +
                  Inventory.new(
         | 
| 131 | 
            +
                    data: Inventory::InventoryStruct.new(
         | 
| 132 | 
            +
                      {
         | 
| 133 | 
            +
                        id: identifier,
         | 
| 134 | 
            +
                        version: "v0",
         | 
| 135 | 
            +
                        type: Inventory::URI_1_1,
         | 
| 136 | 
            +
                        digestAlgorithm: "sha512",
         | 
| 137 | 
            +
                        head: "v0",
         | 
| 138 | 
            +
                        versions: {},
         | 
| 139 | 
            +
                        manifest: {}
         | 
| 140 | 
            +
                      }.tap { |attrs| attrs[:contentDirectory] = @content_directory if @content_directory }
         | 
| 141 | 
            +
                    )
         | 
| 142 | 
            +
                  )
         | 
| 143 | 
            +
                end
         | 
| 8 144 | 
             
              end
         | 
| 9 145 | 
             
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Represents the OCFL version
         | 
| 5 | 
            +
              # https://ocfl.io/1.1/spec/#version
         | 
| 6 | 
            +
              class ObjectVersion < Dry.Struct
         | 
| 7 | 
            +
                # Represents the OCFL user
         | 
| 8 | 
            +
                class User < Dry.Struct
         | 
| 9 | 
            +
                  transform_keys(&:to_sym)
         | 
| 10 | 
            +
                  attribute :name, Types::String
         | 
| 11 | 
            +
                  attribute? :address, Types::String
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                transform_keys(&:to_sym)
         | 
| 15 | 
            +
                attribute :created, Types::String
         | 
| 16 | 
            +
                attribute :state, Types::Hash.map(Types::String, Types::Array.of(Types::String))
         | 
| 17 | 
            +
                attribute? :message, Types::String
         | 
| 18 | 
            +
                attribute? :user, User
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def file_names
         | 
| 21 | 
            +
                  state.values.flatten
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # An OCFL Storage Root is the base directory of an OCFL storage layout.
         | 
| 5 | 
            +
              # https://ocfl.io/1.1/spec/#storage-root
         | 
| 6 | 
            +
              class StorageRoot
         | 
| 7 | 
            +
                attr_reader :base_directory, :layout
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                delegate :path_to, to: :layout
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(base_directory:)
         | 
| 12 | 
            +
                  @base_directory = Pathname.new(base_directory)
         | 
| 13 | 
            +
                  @layout = Layouts::DruidTree
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def exists?
         | 
| 17 | 
            +
                  base_directory.directory?
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def valid?
         | 
| 21 | 
            +
                  namaste_file.exist?
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def save
         | 
| 25 | 
            +
                  # TODO: optionally write the OCFL 1.1 spec
         | 
| 26 | 
            +
                  # TODO: optionally write any given extensions (like the TBD druid-tree layout)
         | 
| 27 | 
            +
                  return if exists? && valid?
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  FileUtils.mkdir_p(base_directory)
         | 
| 30 | 
            +
                  FileUtils.touch(namaste_file)
         | 
| 31 | 
            +
                  true
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def object(identifier, content_directory = nil)
         | 
| 35 | 
            +
                  root = base_directory / path_to(identifier)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  Object.new(identifier:, root:, content_directory:)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                private
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def namaste_file
         | 
| 43 | 
            +
                  base_directory / "0=ocfl_1.1"
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
    
        data/lib/ocfl/version.rb
    CHANGED
    
    
| @@ -0,0 +1,152 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OCFL
         | 
| 4 | 
            +
              # Build a new version
         | 
| 5 | 
            +
              class VersionBuilder
         | 
| 6 | 
            +
                # @params [Object] object
         | 
| 7 | 
            +
                def initialize(object:, overwrite_head: false, state: {})
         | 
| 8 | 
            +
                  @object = object
         | 
| 9 | 
            +
                  @manifest = object.inventory.manifest.dup
         | 
| 10 | 
            +
                  @state = state
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  number = object.head.delete_prefix("v").to_i
         | 
| 13 | 
            +
                  @version_number = "v#{overwrite_head ? number : number + 1}"
         | 
| 14 | 
            +
                  @prepared_content = @prepared = overwrite_head
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                attr_reader :object, :manifest, :state, :version_number
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                delegate :file_names, to: :to_version_struct
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def move_file(incoming_path)
         | 
| 22 | 
            +
                  prepare_content_directory
         | 
| 23 | 
            +
                  already_stored = add(incoming_path)
         | 
| 24 | 
            +
                  return if already_stored
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  FileUtils.mv(incoming_path, content_path)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def copy_file(incoming_path, destination_path: "")
         | 
| 30 | 
            +
                  prepare_content_directory
         | 
| 31 | 
            +
                  copy_one(destination_path.presence || File.basename(incoming_path), incoming_path)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def digest_for_filename(filename)
         | 
| 35 | 
            +
                  state.find { |_, filenames| filenames.include?(filename) }&.first
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                # Note, this only removes the file from this version. Previous versions may still use it.
         | 
| 39 | 
            +
                def delete_file(filename)
         | 
| 40 | 
            +
                  sha512_digest = digest_for_filename(filename)
         | 
| 41 | 
            +
                  raise "Unknown file: #{filename}" unless sha512_digest
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  state.delete(sha512_digest)
         | 
| 44 | 
            +
                  # If the manifest points at the current content directory, then we can delete it.
         | 
| 45 | 
            +
                  file_paths = manifest[sha512_digest]
         | 
| 46 | 
            +
                  return unless file_paths.all? { |path| path.start_with?("#{version_number}/") }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  File.unlink (object.root + file_paths.first).to_s
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                # Copies files into the object and preserves their relative paths as logical directories in the object
         | 
| 52 | 
            +
                def copy_recursive(incoming_path, destination_path: "")
         | 
| 53 | 
            +
                  prepare_content_directory
         | 
| 54 | 
            +
                  incoming_path = incoming_path.delete_suffix("/")
         | 
| 55 | 
            +
                  Dir.glob("#{incoming_path}/**/*").reject { |fn| File.directory?(fn) }.each do |file|
         | 
| 56 | 
            +
                    logical_file_path = file.delete_prefix(incoming_path).delete_prefix("/")
         | 
| 57 | 
            +
                    logical_file_path = File.join(destination_path, logical_file_path) unless destination_path.empty?
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    copy_one(logical_file_path, file)
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def save
         | 
| 64 | 
            +
                  prepare_directory # only necessary if the version has no new content (deletes only)
         | 
| 65 | 
            +
                  write_inventory(build_inventory)
         | 
| 66 | 
            +
                  object.reload
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                private
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def to_version_struct
         | 
| 72 | 
            +
                  ObjectVersion.new(state:, created: Time.now.utc.iso8601)
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def write_inventory(inventory)
         | 
| 76 | 
            +
                  InventoryWriter.new(inventory:, path:).write
         | 
| 77 | 
            +
                  FileUtils.cp(path / "inventory.json", object.root)
         | 
| 78 | 
            +
                  FileUtils.cp(path / "inventory.json.sha512", object.root)
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                # @param [String] logical_file_path where we're going to store the file (e.g. 'object/directory_builder_spec.rb')
         | 
| 82 | 
            +
                # @param [String] incoming_path where's this file from (e.g. 'spec/ocfl/object/directory_builder_spec.rb')
         | 
| 83 | 
            +
                def copy_one(logical_file_path, incoming_path)
         | 
| 84 | 
            +
                  already_stored = add(incoming_path, logical_file_path:)
         | 
| 85 | 
            +
                  return if already_stored
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  parent_dir = (content_path / logical_file_path).parent
         | 
| 88 | 
            +
                  FileUtils.mkdir_p(parent_dir) unless parent_dir == content_path
         | 
| 89 | 
            +
                  FileUtils.cp(incoming_path, content_path / logical_file_path)
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                # @return [Boolean] true if the file already existed in this object. If false, the object must be
         | 
| 93 | 
            +
                #                   moved to the content directory.
         | 
| 94 | 
            +
                def add(incoming_path, logical_file_path: File.basename(incoming_path))
         | 
| 95 | 
            +
                  digest = Digest::SHA512.file(incoming_path).to_s
         | 
| 96 | 
            +
                  version_content_path = content_path.relative_path_from(object.root)
         | 
| 97 | 
            +
                  file_path_relative_to_root = (version_content_path / logical_file_path).to_s
         | 
| 98 | 
            +
                  result = @manifest.key?(digest)
         | 
| 99 | 
            +
                  @manifest[digest] ||= []
         | 
| 100 | 
            +
                  @state[digest] ||= []
         | 
| 101 | 
            +
                  @manifest[digest].push(file_path_relative_to_root)
         | 
| 102 | 
            +
                  @state[digest].push(logical_file_path)
         | 
| 103 | 
            +
                  result
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                def prepare_content_directory
         | 
| 107 | 
            +
                  prepare_directory
         | 
| 108 | 
            +
                  return if @prepared_content
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  FileUtils.mkdir(content_path)
         | 
| 111 | 
            +
                  @prepared_content = true
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def prepare_directory
         | 
| 115 | 
            +
                  return if @prepared
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  FileUtils.mkdir_p(path)
         | 
| 118 | 
            +
                  FileUtils.touch(object.namaste_file) if version_number == "v1"
         | 
| 119 | 
            +
                  @prepared = true
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def content_path
         | 
| 123 | 
            +
                  path / object.inventory.content_directory
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def path
         | 
| 127 | 
            +
                  object.root / version_number
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def build_inventory
         | 
| 131 | 
            +
                  old_data = object.inventory.data
         | 
| 132 | 
            +
                  versions = versions(old_data.versions)
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  # Prune items from manifest if they are not part of any version
         | 
| 135 | 
            +
                  Inventory::InventoryStruct.new(old_data.to_h.merge(manifest: filtered_manifest(versions),
         | 
| 136 | 
            +
                                                                     head: version_number, versions:))
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                # This gives the update list of versions. The old list plus this new one.
         | 
| 140 | 
            +
                # @param [Hash] old_versions the versions prior to this one.
         | 
| 141 | 
            +
                def versions(old_versions)
         | 
| 142 | 
            +
                  old_versions.merge(version_number => to_version_struct)
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                # The manifest after unused SHAs have been filtered out.
         | 
| 146 | 
            +
                def filtered_manifest(versions)
         | 
| 147 | 
            +
                  shas_in_versions = versions.values.flat_map { |v| v.state.keys }.uniq
         | 
| 148 | 
            +
                  manifest.slice!(*shas_in_versions)
         | 
| 149 | 
            +
                  manifest
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
              end
         | 
| 152 | 
            +
            end
         | 
    
        data/tmp/.keep
    ADDED
    
    | 
            File without changes
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: ocfl
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.9.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Justin Coyne
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2024- | 
| 11 | 
            +
            date: 2024-06-03 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -92,17 +92,18 @@ files: | |
| 92 92 | 
             
            - README.md
         | 
| 93 93 | 
             
            - Rakefile
         | 
| 94 94 | 
             
            - lib/ocfl.rb
         | 
| 95 | 
            +
            - lib/ocfl/inventory.rb
         | 
| 96 | 
            +
            - lib/ocfl/inventory_loader.rb
         | 
| 97 | 
            +
            - lib/ocfl/inventory_validator.rb
         | 
| 98 | 
            +
            - lib/ocfl/inventory_writer.rb
         | 
| 99 | 
            +
            - lib/ocfl/layouts/druid_tree.rb
         | 
| 95 100 | 
             
            - lib/ocfl/object.rb
         | 
| 96 | 
            -
            - lib/ocfl/ | 
| 97 | 
            -
            - lib/ocfl/ | 
| 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
         | 
| 101 | 
            +
            - lib/ocfl/object_version.rb
         | 
| 102 | 
            +
            - lib/ocfl/storage_root.rb
         | 
| 104 103 | 
             
            - lib/ocfl/version.rb
         | 
| 104 | 
            +
            - lib/ocfl/version_builder.rb
         | 
| 105 105 | 
             
            - sig/ocfl.rbs
         | 
| 106 | 
            +
            - tmp/.keep
         | 
| 106 107 | 
             
            homepage: https://github.com/sul-dlss/ocfl-rb
         | 
| 107 108 | 
             
            licenses: []
         | 
| 108 109 | 
             
            metadata:
         | 
| @@ -1,107 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OCFL
         | 
| 4 | 
            -
              module Object
         | 
| 5 | 
            -
                # An OCFL Directory layout for a particular object.
         | 
| 6 | 
            -
                class Directory
         | 
| 7 | 
            -
                  # @param [String] object_root
         | 
| 8 | 
            -
                  # @param [Inventory] inventory this is only passed in when creating a new object. (see DirectoryBuilder)
         | 
| 9 | 
            -
                  def initialize(object_root:, inventory: nil)
         | 
| 10 | 
            -
                    @object_root = Pathname.new(object_root)
         | 
| 11 | 
            -
                    @version_inventory = {}
         | 
| 12 | 
            -
                    @version_inventory_errors = {}
         | 
| 13 | 
            -
                    @inventory = inventory
         | 
| 14 | 
            -
                  end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                  attr_reader :object_root, :errors
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                  delegate :head, :versions, :manifest, to: :inventory
         | 
| 19 | 
            -
             | 
| 20 | 
            -
                  def path(filepath:, version: nil)
         | 
| 21 | 
            -
                    version ||= head
         | 
| 22 | 
            -
                    relative_path = version_inventory(version).path(filepath)
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                    raise FileNotFound, "Path '#{filepath}' not found in #{version} inventory" if relative_path.nil?
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                    object_root / relative_path
         | 
| 27 | 
            -
                  end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                  def inventory
         | 
| 30 | 
            -
                    @inventory ||= begin
         | 
| 31 | 
            -
                      data = InventoryLoader.load(object_root / "inventory.json")
         | 
| 32 | 
            -
                      if data.success?
         | 
| 33 | 
            -
                        Inventory.new(data: data.value!)
         | 
| 34 | 
            -
                      else
         | 
| 35 | 
            -
                        @errors = data.failure
         | 
| 36 | 
            -
                        puts @errors.messages.inspect
         | 
| 37 | 
            -
                        nil
         | 
| 38 | 
            -
                      end
         | 
| 39 | 
            -
                    end
         | 
| 40 | 
            -
                  end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                  def head_inventory
         | 
| 43 | 
            -
                    version_inventory(inventory.head)
         | 
| 44 | 
            -
                  end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  def version_inventory(version)
         | 
| 47 | 
            -
                    @version_inventory[version] ||= begin
         | 
| 48 | 
            -
                      data = InventoryLoader.load(object_root / version / "inventory.json")
         | 
| 49 | 
            -
                      if data.success?
         | 
| 50 | 
            -
                        Inventory.new(data: data.value!)
         | 
| 51 | 
            -
                      else
         | 
| 52 | 
            -
                        @version_inventory_errors[version] = data.failure
         | 
| 53 | 
            -
                        puts @version_inventory_errors[version].messages.inspect
         | 
| 54 | 
            -
                        nil
         | 
| 55 | 
            -
                      end
         | 
| 56 | 
            -
                    end
         | 
| 57 | 
            -
                  end
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                  def reload
         | 
| 60 | 
            -
                    @version_inventory = {}
         | 
| 61 | 
            -
                    @inventory = nil
         | 
| 62 | 
            -
                    @errors = nil
         | 
| 63 | 
            -
                    @version_inventory_errors = {}
         | 
| 64 | 
            -
                    true
         | 
| 65 | 
            -
                  end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                  # Start a completely new version
         | 
| 68 | 
            -
                  def begin_new_version
         | 
| 69 | 
            -
                    DraftVersion.new(object_directory: self, state: head_inventory.state)
         | 
| 70 | 
            -
                  end
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                  # Get a handle for the head version
         | 
| 73 | 
            -
                  def head_version
         | 
| 74 | 
            -
                    DraftVersion.new(object_directory: self, overwrite_head: true, state: head_inventory.state)
         | 
| 75 | 
            -
                  end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                  # Get a handle that will replace the existing head version
         | 
| 78 | 
            -
                  def overwrite_current_version
         | 
| 79 | 
            -
                    DraftVersion.new(object_directory: self, overwrite_head: true)
         | 
| 80 | 
            -
                  end
         | 
| 81 | 
            -
             | 
| 82 | 
            -
                  def exists?
         | 
| 83 | 
            -
                    namaste_exists?
         | 
| 84 | 
            -
                  end
         | 
| 85 | 
            -
             | 
| 86 | 
            -
                  def valid?
         | 
| 87 | 
            -
                    InventoryValidator.new(directory: object_root).valid? &&
         | 
| 88 | 
            -
                      namaste_exists? &&
         | 
| 89 | 
            -
                      !inventory.nil? && # Ensures it could be loaded
         | 
| 90 | 
            -
                      head_directory_valid?
         | 
| 91 | 
            -
                  end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                  def head_directory_valid?
         | 
| 94 | 
            -
                    InventoryValidator.new(directory: object_root / inventory.head).valid? &&
         | 
| 95 | 
            -
                      !head_inventory.nil? # Ensures it could be loaded
         | 
| 96 | 
            -
                  end
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                  def namaste_exists?
         | 
| 99 | 
            -
                    File.exist?(namaste_file)
         | 
| 100 | 
            -
                  end
         | 
| 101 | 
            -
             | 
| 102 | 
            -
                  def namaste_file
         | 
| 103 | 
            -
                    object_root / "0=ocfl_object_1.1"
         | 
| 104 | 
            -
                  end
         | 
| 105 | 
            -
                end
         | 
| 106 | 
            -
              end
         | 
| 107 | 
            -
            end
         | 
| @@ -1,69 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OCFL
         | 
| 4 | 
            -
              module Object
         | 
| 5 | 
            -
                # Creates a OCFL Directory layout for a particular object.
         | 
| 6 | 
            -
                class DirectoryBuilder
         | 
| 7 | 
            -
                  class ObjectExists < Error; end
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                  def initialize(object_root:, id:, content_directory: nil)
         | 
| 10 | 
            -
                    @object_root = Pathname.new(object_root)
         | 
| 11 | 
            -
                    raise ObjectExists, "The directory `#{object_root}' already exists" if @object_root.exist?
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    @id = id
         | 
| 14 | 
            -
                    inventory = Inventory.new(
         | 
| 15 | 
            -
                      data: Inventory::InventoryStruct.new(
         | 
| 16 | 
            -
                        new_inventory_attrs.tap { |attrs| attrs[:contentDirectory] = content_directory if content_directory }
         | 
| 17 | 
            -
                      )
         | 
| 18 | 
            -
                    )
         | 
| 19 | 
            -
                    @object_directory = Directory.new(object_root:, inventory:)
         | 
| 20 | 
            -
                  end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  attr_reader :id, :inventory, :object_root, :object_directory
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                  def copy_file(...)
         | 
| 25 | 
            -
                    create_object_directory
         | 
| 26 | 
            -
                    version.copy_file(...)
         | 
| 27 | 
            -
                  end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                  def copy_recursive(...)
         | 
| 30 | 
            -
                    create_object_directory
         | 
| 31 | 
            -
                    version.copy_recursive(...)
         | 
| 32 | 
            -
                  end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                  def create_object_directory
         | 
| 35 | 
            -
                    FileUtils.mkdir_p(object_root)
         | 
| 36 | 
            -
                    FileUtils.touch(object_directory.namaste_file) unless File.exist?(object_directory.namaste_file)
         | 
| 37 | 
            -
                  end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                  # @return [Directory]
         | 
| 40 | 
            -
                  def save
         | 
| 41 | 
            -
                    create_object_directory
         | 
| 42 | 
            -
                    write_inventory
         | 
| 43 | 
            -
                    object_directory
         | 
| 44 | 
            -
                  end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  def version
         | 
| 47 | 
            -
                    @version ||= DraftVersion.new(object_directory:)
         | 
| 48 | 
            -
                  end
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                  def write_inventory
         | 
| 51 | 
            -
                    version.save
         | 
| 52 | 
            -
                  end
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                  private
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                  def new_inventory_attrs
         | 
| 57 | 
            -
                    {
         | 
| 58 | 
            -
                      id:,
         | 
| 59 | 
            -
                      version: "v0",
         | 
| 60 | 
            -
                      type: Inventory::URI_1_1,
         | 
| 61 | 
            -
                      digestAlgorithm: "sha512",
         | 
| 62 | 
            -
                      head: "v0",
         | 
| 63 | 
            -
                      versions: {},
         | 
| 64 | 
            -
                      manifest: {}
         | 
| 65 | 
            -
                    }
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
                end
         | 
| 68 | 
            -
              end
         | 
| 69 | 
            -
            end
         | 
| @@ -1,143 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OCFL
         | 
| 4 | 
            -
              module Object
         | 
| 5 | 
            -
                # A new OCFL version
         | 
| 6 | 
            -
                class DraftVersion
         | 
| 7 | 
            -
                  # @params [Directory] object_directory
         | 
| 8 | 
            -
                  def initialize(object_directory:, overwrite_head: false, state: {})
         | 
| 9 | 
            -
                    @object_directory = object_directory
         | 
| 10 | 
            -
                    @manifest = object_directory.inventory.manifest.dup
         | 
| 11 | 
            -
                    @state = state
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    number = object_directory.head.delete_prefix("v").to_i
         | 
| 14 | 
            -
                    @version_number = "v#{overwrite_head ? number : number + 1}"
         | 
| 15 | 
            -
                    @prepared_content = @prepared = overwrite_head
         | 
| 16 | 
            -
                  end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                  attr_reader :object_directory, :manifest, :state, :version_number
         | 
| 19 | 
            -
             | 
| 20 | 
            -
                  delegate :file_names, to: :to_version_struct
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  def move_file(incoming_path)
         | 
| 23 | 
            -
                    prepare_content_directory
         | 
| 24 | 
            -
                    add(incoming_path)
         | 
| 25 | 
            -
                    FileUtils.mv(incoming_path, content_path)
         | 
| 26 | 
            -
                  end
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                  def copy_file(incoming_path, destination_path: "")
         | 
| 29 | 
            -
                    prepare_content_directory
         | 
| 30 | 
            -
                    copy_one(destination_path.presence || File.basename(incoming_path), incoming_path)
         | 
| 31 | 
            -
                  end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                  def digest_for_filename(filename)
         | 
| 34 | 
            -
                    state.find { |_, filenames| filenames.include?(filename) }&.first
         | 
| 35 | 
            -
                  end
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                  # Note, this only removes the file from this version. Previous versions may still use it.
         | 
| 38 | 
            -
                  def delete_file(filename)
         | 
| 39 | 
            -
                    sha512_digest = digest_for_filename(filename)
         | 
| 40 | 
            -
                    raise "Unknown file: #{filename}" unless sha512_digest
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                    state.delete(sha512_digest)
         | 
| 43 | 
            -
                    # If the manifest points at the current content directory, then we can delete it.
         | 
| 44 | 
            -
                    file_paths = manifest[sha512_digest]
         | 
| 45 | 
            -
                    return unless file_paths.all? { |path| path.start_with?("#{version_number}/") }
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                    File.unlink (object_directory.object_root + file_paths.first).to_s
         | 
| 48 | 
            -
                  end
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                  # Copies files into the object and preserves their relative paths as logical directories in the object
         | 
| 51 | 
            -
                  def copy_recursive(incoming_path, destination_path: "")
         | 
| 52 | 
            -
                    prepare_content_directory
         | 
| 53 | 
            -
                    incoming_path = incoming_path.delete_suffix("/")
         | 
| 54 | 
            -
                    Dir.glob("#{incoming_path}/**/*").reject { |fn| File.directory?(fn) }.each do |file|
         | 
| 55 | 
            -
                      logical_file_path = file.delete_prefix(incoming_path).delete_prefix("/")
         | 
| 56 | 
            -
                      logical_file_path = File.join(destination_path, logical_file_path) unless destination_path.empty?
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                      copy_one(logical_file_path, file)
         | 
| 59 | 
            -
                    end
         | 
| 60 | 
            -
                  end
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                  def save
         | 
| 63 | 
            -
                    prepare_directory # only necessary if the version has no new content (deletes only)
         | 
| 64 | 
            -
                    write_inventory(build_inventory)
         | 
| 65 | 
            -
                    object_directory.reload
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                  def to_version_struct
         | 
| 69 | 
            -
                    Version.new(state:, created: Time.now.utc.iso8601)
         | 
| 70 | 
            -
                  end
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                  private
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                  def write_inventory(inventory)
         | 
| 75 | 
            -
                    InventoryWriter.new(inventory:, path:).write
         | 
| 76 | 
            -
                    FileUtils.cp(path / "inventory.json", object_directory.object_root)
         | 
| 77 | 
            -
                    FileUtils.cp(path / "inventory.json.sha512", object_directory.object_root)
         | 
| 78 | 
            -
                  end
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                  # @param [String] logical_file_path where we're going to store the file (e.g. 'object/directory_builder_spec.rb')
         | 
| 81 | 
            -
                  # @param [String] incoming_path where's this file from (e.g. 'spec/ocfl/object/directory_builder_spec.rb')
         | 
| 82 | 
            -
                  def copy_one(logical_file_path, incoming_path)
         | 
| 83 | 
            -
                    add(incoming_path, logical_file_path:)
         | 
| 84 | 
            -
                    parent_dir = (content_path / logical_file_path).parent
         | 
| 85 | 
            -
                    FileUtils.mkdir_p(parent_dir) unless parent_dir == content_path
         | 
| 86 | 
            -
                    FileUtils.cp(incoming_path, content_path / logical_file_path)
         | 
| 87 | 
            -
                  end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                  def add(incoming_path, logical_file_path: File.basename(incoming_path))
         | 
| 90 | 
            -
                    digest = Digest::SHA512.file(incoming_path).to_s
         | 
| 91 | 
            -
                    version_content_path = content_path.relative_path_from(object_directory.object_root)
         | 
| 92 | 
            -
                    file_path_relative_to_root = (version_content_path / logical_file_path).to_s
         | 
| 93 | 
            -
                    @manifest[digest] = [file_path_relative_to_root]
         | 
| 94 | 
            -
                    @state[digest] = [logical_file_path]
         | 
| 95 | 
            -
                  end
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                  def prepare_content_directory
         | 
| 98 | 
            -
                    prepare_directory
         | 
| 99 | 
            -
                    return if @prepared_content
         | 
| 100 | 
            -
             | 
| 101 | 
            -
                    FileUtils.mkdir(content_path)
         | 
| 102 | 
            -
                    @prepared_content = true
         | 
| 103 | 
            -
                  end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                  def prepare_directory
         | 
| 106 | 
            -
                    return if @prepared
         | 
| 107 | 
            -
             | 
| 108 | 
            -
                    FileUtils.mkdir(path)
         | 
| 109 | 
            -
                    @prepared = true
         | 
| 110 | 
            -
                  end
         | 
| 111 | 
            -
             | 
| 112 | 
            -
                  def content_path
         | 
| 113 | 
            -
                    path / object_directory.inventory.content_directory
         | 
| 114 | 
            -
                  end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                  def path
         | 
| 117 | 
            -
                    object_directory.object_root / version_number
         | 
| 118 | 
            -
                  end
         | 
| 119 | 
            -
             | 
| 120 | 
            -
                  def build_inventory
         | 
| 121 | 
            -
                    old_data = object_directory.inventory.data
         | 
| 122 | 
            -
                    versions = versions(old_data.versions)
         | 
| 123 | 
            -
             | 
| 124 | 
            -
                    # Prune items from manifest if they are not part of any version
         | 
| 125 | 
            -
                    Inventory::InventoryStruct.new(old_data.to_h.merge(manifest: filtered_manifest(versions),
         | 
| 126 | 
            -
                                                                       head: version_number, versions:))
         | 
| 127 | 
            -
                  end
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                  # This gives the update list of versions. The old list plus this new one.
         | 
| 130 | 
            -
                  # @param [Hash] old_versions the versions prior to this one.
         | 
| 131 | 
            -
                  def versions(old_versions)
         | 
| 132 | 
            -
                    old_versions.merge(version_number => to_version_struct)
         | 
| 133 | 
            -
                  end
         | 
| 134 | 
            -
             | 
| 135 | 
            -
                  # The manifest after unused SHAs have been filtered out.
         | 
| 136 | 
            -
                  def filtered_manifest(versions)
         | 
| 137 | 
            -
                    shas_in_versions = versions.values.flat_map { |v| v.state.keys }.uniq
         | 
| 138 | 
            -
                    manifest.slice!(*shas_in_versions)
         | 
| 139 | 
            -
                    manifest
         | 
| 140 | 
            -
                  end
         | 
| 141 | 
            -
                end
         | 
| 142 | 
            -
              end
         | 
| 143 | 
            -
            end
         | 
| @@ -1,49 +0,0 @@ | |
| 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 | 
            -
                  delegate :state, to: :head_version
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                  def content_directory
         | 
| 32 | 
            -
                    data.contentDirectory || "content"
         | 
| 33 | 
            -
                  end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                  # @return [String,nil] the path to the file relative to the object root. (e.g. v2/content/image.tiff)
         | 
| 36 | 
            -
                  def path(logical_path)
         | 
| 37 | 
            -
                    digest, = state.find { |_, logical_paths| logical_paths.include?(logical_path) }
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                    return unless digest
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                    manifest[digest].find { |content_path| content_path.match(%r{\Av\d+/#{content_directory}/#{logical_path}\z}) }
         | 
| 42 | 
            -
                  end
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                  def head_version
         | 
| 45 | 
            -
                    versions[head]
         | 
| 46 | 
            -
                  end
         | 
| 47 | 
            -
                end
         | 
| 48 | 
            -
              end
         | 
| 49 | 
            -
            end
         | 
| @@ -1,45 +0,0 @@ | |
| 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
         | 
| @@ -1,42 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OCFL
         | 
| 4 | 
            -
              module Object
         | 
| 5 | 
            -
                # Checks to see that the inventory.json and it's checksum in a direcotory are valid
         | 
| 6 | 
            -
                class InventoryValidator
         | 
| 7 | 
            -
                  def initialize(directory:)
         | 
| 8 | 
            -
                    @directory = Pathname.new(directory)
         | 
| 9 | 
            -
                  end
         | 
| 10 | 
            -
             | 
| 11 | 
            -
                  attr_reader :directory
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                  def valid?
         | 
| 14 | 
            -
                    inventory_file_exists? && inventory_file_matches_checksum?
         | 
| 15 | 
            -
                  end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                  def inventory_file_exists?
         | 
| 18 | 
            -
                    File.exist?(inventory_file)
         | 
| 19 | 
            -
                  end
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                  def inventory_file_matches_checksum?
         | 
| 22 | 
            -
                    return false unless File.exist?(inventory_checksum_file)
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                    actual = inventory_file_checksum
         | 
| 25 | 
            -
                    expected = File.read(inventory_checksum_file)
         | 
| 26 | 
            -
                    expected.match?(/\A#{actual}\s+inventory\.json\z/)
         | 
| 27 | 
            -
                  end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                  def inventory_checksum_file
         | 
| 30 | 
            -
                    directory / "inventory.json.sha512"
         | 
| 31 | 
            -
                  end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                  def inventory_file_checksum
         | 
| 34 | 
            -
                    Digest::SHA512.file inventory_file
         | 
| 35 | 
            -
                  end
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                  def inventory_file
         | 
| 38 | 
            -
                    directory / "inventory.json"
         | 
| 39 | 
            -
                  end
         | 
| 40 | 
            -
                end
         | 
| 41 | 
            -
              end
         | 
| 42 | 
            -
            end
         | 
| @@ -1,37 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OCFL
         | 
| 4 | 
            -
              module Object
         | 
| 5 | 
            -
                # Writes a OCFL Inventory to json on disk
         | 
| 6 | 
            -
                class InventoryWriter
         | 
| 7 | 
            -
                  def initialize(inventory:, path:)
         | 
| 8 | 
            -
                    @path = path
         | 
| 9 | 
            -
                    @inventory = inventory
         | 
| 10 | 
            -
                  end
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                  attr_reader :inventory, :path
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                  def write
         | 
| 15 | 
            -
                    write_inventory
         | 
| 16 | 
            -
                    update_inventory_checksum
         | 
| 17 | 
            -
                  end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                  def write_inventory
         | 
| 20 | 
            -
                    File.write(inventory_file, JSON.pretty_generate(inventory.to_h))
         | 
| 21 | 
            -
                  end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                  def inventory_file
         | 
| 24 | 
            -
                    path / "inventory.json"
         | 
| 25 | 
            -
                  end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                  def checksum_file
         | 
| 28 | 
            -
                    path / "inventory.json.sha512"
         | 
| 29 | 
            -
                  end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                  def update_inventory_checksum
         | 
| 32 | 
            -
                    digest = Digest::SHA512.file inventory_file
         | 
| 33 | 
            -
                    File.write(checksum_file, "#{digest} inventory.json")
         | 
| 34 | 
            -
                  end
         | 
| 35 | 
            -
                end
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
            end
         | 
    
        data/lib/ocfl/object/version.rb
    DELETED
    
    | @@ -1,26 +0,0 @@ | |
| 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 | 
            -
             | 
| 21 | 
            -
                  def file_names
         | 
| 22 | 
            -
                    state.values.flatten
         | 
| 23 | 
            -
                  end
         | 
| 24 | 
            -
                end
         | 
| 25 | 
            -
              end
         | 
| 26 | 
            -
            end
         |