class OcflTools::OcflObject

Class that represents the data structures used by an OCFL inventory file.

Attributes

contentDirectory[RW]

@return [String] the name of the directory, inside each version directory, that the OCFL object should use as the base directory for files.

digestAlgorithm[RW]

@return [String] algorithm used by the OCFL object to generate digests for file manifests and versions.

fixity[RW]

@return [Hash] fixity block of the OCFL object.

head[RW]

@return [String] the most recent version of the OCFL object, expressed as a string that conforms to the format defined in version_format.

id[RW]

@return [String] id the unique identifer of the OCFL object, as defined by the local repository system.

manifest[RW]

@return [Hash] manifest block of the OCFL object.

type[RW]

@return [String] the version of the OCFL spec to which this object conforms, expressed as a URL, as required by the OCFL specification.

versions[RW]

@return [Hash] versions block of the OCFL object.

Public Class Methods

new() click to toggle source
# File lib/ocfl_tools/ocfl_object.rb, line 30
def initialize
  # Parameters that must be serialized into JSON
  @id               = nil
  @head             = nil
  @type             = nil # OcflTools.config.content_type
  @digestAlgorithm  = OcflTools.config.digest_algorithm # sha512 is recommended, Stanford uses sha256.
  @contentDirectory = OcflTools.config.content_directory # default is 'content', Stanford uses 'data'
  @manifest         = {}
  @versions         = {} # A hash of Version hashes.
  @fixity           = {} # Optional. Same format as Manifest.
end

Public Instance Methods

add_file(file, digest, version) click to toggle source

Adds a file to a version. @param [Pathname] file is the logical filename within the object. @param [String] digest of filename, presumably computed with the {digestAlgorithm} for the object. @param [Integer] version to add file to. @return [Hash] state block reflecting the version after the changes. @note will raise an error if an attempt is made to add a file to a prior (non-head) version. Will also raise an error if the requested file already exists in this version with a different digest: use {update_file} instead.

# File lib/ocfl_tools/ocfl_object.rb, line 181
def add_file(file, digest, version)
  # We use get_state here instead of asking @versions directly
  # because get_state will create version hash if it doesn't already exist.
  my_state = get_state(version)

  unless version == version_id_list.max
    raise OcflTools::Errors::CannotEditPreviousVersion, "Can't edit prior versions! Only version #{version_id_list.max} can be modified now."
  end

  # if the key is not in the manifest, assume that we meant to add it.
  update_manifest(file, digest, version) unless @manifest.key?(digest)

  if my_state.key?(digest)
    # file's already in this version. Add file to existing digest.
    my_files = my_state[digest]
    my_files << file
    unique_files = my_files.uniq # Just in case we're trying to add the same thing multiple times.
    # Need to actually add this to @versions!
    @versions[OcflTools::Utils.version_int_to_string(version)]['state'][digest] = unique_files
    # Prove we actually added to state
    return get_state(version)
  end

  # Check to make sure the file isn't already in this state with a different digest!
  # If so; fail. We don't do implicit / soft adds. You want that, be explict: do an update_file instead.
  existing_files = get_files(version)
  if existing_files.key?(file)
    raise OcflTools::Errors::FileDigestMismatch, "#{file} already exists with different digest in version #{version}. Consider update instead."
  end

  # if it's not in State already, just add it.
  @versions[OcflTools::Utils.version_int_to_string(version)]['state'][digest] = [file]

  get_state(version)
end
copy_file(source_file, destination_file, version) click to toggle source

Copies a file within the same version. If the destination file already exists with a different digest, it is overwritten with the digest of the source file. @param [Filepath] source_file filepath of source file. @param [Filepath] destination_file filepath of destination file. @param [Integer] version version of OCFL object. @return [Hash] state block of version after file copy has completed. @note Raises an error if source_file does not exist in this version.

# File lib/ocfl_tools/ocfl_object.rb, line 317
def copy_file(source_file, destination_file, version)
  # add new filename to existing digest in current state.
  # If destination file already exists, overwrite it.
  existing_files = get_files(version)

  if existing_files.key?(destination_file)
    delete_file(destination_file, version)
  end
  # should NOT call add_file, as add_file updates the manifest.
  # Should instead JUST update current state with new filepath.
  digest = get_digest(source_file, version) # errors out if source_file not found in current state

  my_state = get_state(version)
  my_files = my_state[digest]
  my_files << destination_file
  unique_files = my_files.uniq # Just in case we're trying to add the same thing multiple times.
  # Need to actually add this to @versions!
  @versions[OcflTools::Utils.version_int_to_string(version)]['state'][digest] = unique_files
  # Prove we actually added to state
  get_state(version)
  # self.add_file(destination_file, self.get_digest(source_file, version), version)
end
create_version_hash() click to toggle source

Returns a version hash with the correct keys created, ready for content to be added. @return [Hash] empty version Hash with 'created', 'message', 'user' and 'state' keys. @note internal API

# File lib/ocfl_tools/ocfl_object.rb, line 400
def create_version_hash
  new_version = {}
  new_version['created'] = ''
  new_version['message'] = ''
  new_version['user'] = {}
  # user is #name, # address.
  new_version['user']['name'] = ''
  new_version['user']['address'] = ''
  new_version['state'] = {}
  new_version
end
delete_file(file, version) click to toggle source

Given a filepath, deletes that file from the given version. If multiple copies of the same file (as identified by a common digest) exist in the version, only the requested filepath is removed. @param [Pathname] file logical path of file to be deleted. @param [Integer] version version of object to delete file from. @return [Hash] state of version after delete has completed.

# File lib/ocfl_tools/ocfl_object.rb, line 287
def delete_file(file, version)
  # remove filename, may remove digest if that was last file associated with that digest.
  my_state = get_state(version) # Creates version & copies state from prior version if doesn't exist.

  unless version == version_id_list.max
    raise OcflTools::Errors::CannotEditPreviousVersion, "Can't edit prior versions! Only version #{version_id_list.max} can be modified now."
  end

  my_digest = get_digest(file, version)
  # we know it's here b/c self.get_digest would have crapped out if not.
  my_array = my_state[my_digest]  # Get [Array] of files that have this digest in this version.
  my_array.delete(file)           # Delete the array value that matches file.
  if !my_array.empty?
    # update the array with (fewer) items.
    my_state[my_digest] = my_array
  else
    # delete the key.
    my_state.delete(my_digest)
  end
  # put results back into State.
  set_state(version, my_state)
end
get_current_files() click to toggle source

Gets all files for the current (highest) version of the OCFL object. Represents the state of the object at 'head', with the logical files that consist of the most recent version and their physical representations on disk, relative to the object's root directory. @return [Hash] of files from most recent version, with logical file as key, associated physical filepath as value.

# File lib/ocfl_tools/ocfl_object.rb, line 171
def get_current_files
  get_files(OcflTools::Utils.version_string_to_int(@head))
end
get_digest(file, version) click to toggle source

When given a file path and version, return the associated digest from version state. @param [Pathname] file filepath of file to return digest for. @param [Integer] version version of OCFL object to search for the requested file. @return [String] digest of requested file. @note Will raise an exception if requested filepath is not in given version.

# File lib/ocfl_tools/ocfl_object.rb, line 356
def get_digest(file, version)
  # Make a hash with each individual file as a key, with the appropriate digest as value.
  inverted = get_state(version).invert
  my_files = {}
  inverted.each do |files, digest|
    files.each do |i_file|
      my_files[i_file] = digest
    end
  end
  # Now see if the requested file is actually here.
  unless my_files.key?(file)
    raise OcflTools::Errors::FileMissingFromVersionState, "Get_digest can't find requested file #{file} in version #{version}."
  end

  my_files[file]
end
get_files(version) click to toggle source

Gets a hash of all logical files and their associated physical filepaths with the given version. @param [Integer] version from which to generate file list. @return [Hash] of files, with logical file as key, physical location within object dir as value.

# File lib/ocfl_tools/ocfl_object.rb, line 152
def get_files(version)
  my_state = get_state(version)
  my_files = {}

  my_state.each do |digest, filepaths| # filepaths is [Array]
    filepaths.each do |logical_filepath|
      # look up this file via digest in @manifest.
      physical_filepath = @manifest[digest]
      # physical_filepath is an [Array] of files, but they're all the same so only need 1.
      my_files[logical_filepath] = physical_filepath[0]
    end
  end
  my_files
end
get_state(version) click to toggle source

Gets the state block of a given version, comprising of digest keys and an array of filenames associated with those digests. @param [Integer] version of OCFL object to retreive version state block of. @return [Hash] of digests and array of pathnames associated with this version. @note Creates new version and copies previous versions' state block over if requested version does not yet exist.

# File lib/ocfl_tools/ocfl_object.rb, line 135
def get_state(version)
  my_version = get_version(version)
  my_version['state']
end
get_version(version) click to toggle source

Gets the existing version hash for the requested version, or else creates and populates a new, empty version hash. @param [Integer] version @return [Hash] version block, if it exists, or creates new with prior version state in it. @note If a (n-1) version exists in the object, and the requested version does not yet exist, this method will copy that version's state block into the new version.

# File lib/ocfl_tools/ocfl_object.rb, line 378
def get_version(version)
  unless version > 0
    raise OcflTools::Errors::NonCompliantValue, "Requested value '#{version}' for object version does not comply with specification."
  end
  if @versions.key?(OcflTools::Utils.version_int_to_string(version))
    @versions[OcflTools::Utils.version_int_to_string(version)]
  else
    # Otherwise, construct a new Version [Hash] and return that.
    @versions[OcflTools::Utils.version_int_to_string(version)] = create_version_hash

    # If version -1 exists, copy prior version state over.
    if @versions.key?(OcflTools::Utils.version_int_to_string(version - 1))
      @versions[OcflTools::Utils.version_int_to_string(version)]['state'] = OcflTools::Utils.deep_copy(@versions[OcflTools::Utils.version_int_to_string(version - 1)]['state'])
    end

    @versions[OcflTools::Utils.version_int_to_string(version)]
  end
end
get_version_created(version) click to toggle source

returns the created field for a given version. @param [Integer] version of OCFL object to get value for. @return [String] created value set for the given version, if any. @note will raise an exception if you attempt to query a non-existent version.

# File lib/ocfl_tools/ocfl_object.rb, line 89
def get_version_created(version)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['created']
end
get_version_message(version) click to toggle source

returns the message field for a given version. @param [Integer] version of OCFL object to get the message for. @return [String] message set for the given version, if any. @note will raise an exception if you attempt to query a non-existent version.

# File lib/ocfl_tools/ocfl_object.rb, line 65
def get_version_message(version)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['message']
end
get_version_user(version) click to toggle source

Gets the user Hash for a given version. @ param [Integer] version of OCFL object to retrieve user block for. @return [Hash] user block for this version, a hash consisting of two keys, 'name' and 'address'. @note will raise an exception if you attempt to query a nonexistent version.

# File lib/ocfl_tools/ocfl_object.rb, line 113
def get_version_user(version)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['user']
end
move_file(old_file, new_file, version) click to toggle source

Moves (renames) a file from one location to another within the same version. @param [Pathname] old_file filepath to move. @param [Pathname] new_file new filepath. @return [Hash] state block of version after file copy has completed. @note This is functionally a {copy_file} followed by a {delete_file}. Will raise an error if the source file does not exist in this version.

# File lib/ocfl_tools/ocfl_object.rb, line 345
def move_file(old_file, new_file, version)
  # re-name; functionally a copy and delete.
  copy_file(old_file, new_file, version)
  delete_file(old_file, version)
end
set_head_from_version(version) click to toggle source

sets @head in current string format, when given integer. @param [Integer] version to set head to. @return {@head} value of most recent version.

# File lib/ocfl_tools/ocfl_object.rb, line 45
def set_head_from_version(version)
  @head = OcflTools::Utils.version_int_to_string(version)
end
set_state(version, hash) click to toggle source

Sets the state block for a given version when provided with a hash of digest keys and an array of associated filenames. @param [Integer] version of object to set state for. @param [Hash] hash of digests (keys) and an array of pathnames (values) associated with those digests. @note It is prefered to update version state via add/update/delete/copy/move file operations.

# File lib/ocfl_tools/ocfl_object.rb, line 144
def set_state(version, hash)
  # SAN Check needed here to make sure passed Hash has all expected keys.
  @versions[OcflTools::Utils.version_int_to_string(version)]['state'] = hash
end
set_version(version, hash) click to toggle source

When given a correctly-constructed hash, create a new OCFL version. See {create_version_hash} for more context. @param [Integer] version create a new OCFL version block with this version number. @param [Hash] hash use this hash for the content of the new OCFL version block.

# File lib/ocfl_tools/ocfl_object.rb, line 415
def set_version(version, hash)
  # SAN Check to make sure passed Hash has all expected keys.
  e216_errors = []
  %w[created message user state].each do |key|
    if hash.key?(key) == false
      e216_errors << "version #{version} hash block is missing required #{key} key."
    end
  end
  if e216_errors.size > 0
    raise OcflTools::Errors::ValidationError, details: { "E216" => e216_errors }
  end
  @versions[OcflTools::Utils.version_int_to_string(version)] = hash
end
set_version_created(version, created) click to toggle source

sets the created field for a given version. @param [Integer] version of OCFL object to set value for. @param [String] created value to set for given version. @note will raise an exception if you attempt to query a non-existent version.

# File lib/ocfl_tools/ocfl_object.rb, line 77
def set_version_created(version, created)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['created'] = created
end
set_version_message(version, message) click to toggle source

sets the message field for a given version. @param [Integer] version of OCFL object to set message for. @param [String] message to set for given version. @note will raise an exception if you attempt to query a non-existent version.

# File lib/ocfl_tools/ocfl_object.rb, line 53
def set_version_message(version, message)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['message'] = message
end
set_version_user(version, user) click to toggle source

Sets the user Hash for a given version. Expects a complete User hash (with sub-keys of name & address). @param [Integer] version of OCFL object to set the user block for. @param [Hash] user block to set for this version. Must be a hash with two keys 'name' and 'address'. @note will raise an exception if you attempt to query a nonexistent version.

# File lib/ocfl_tools/ocfl_object.rb, line 101
def set_version_user(version, user)
  unless @versions.key?(OcflTools::Utils.version_int_to_string(version))
    raise OcflTools::Errors::RequestedKeyNotFound, "Version #{version} does not yet exist!"
  end

  @versions[OcflTools::Utils.version_int_to_string(version)]['user'] = user
end
update_file(file, digest, version) click to toggle source

Updates an existing file with a new bitstream and digest. @param [String] file filepath to update. @param [String] digest of updated file. @param [Integer] version of object to update. @note this method explicitly deletes the prior file if found, and re-creates it with a new digest via the {add_file} method.

# File lib/ocfl_tools/ocfl_object.rb, line 222
def update_file(file, digest, version)
  # Same filename, different digest, update manifest.
  # Do a Delete, then an Add.
  existing_files = get_files(version)

  delete_file(file, version) if existing_files.key?(file)
  add_file(file, digest, version)
end
update_fixity(digest, fixityAlgorithm, fixityDigest) click to toggle source

Given a digest, fixityAlgo and fixityDigest, add to fixity block. @param [String] digest value from Manifest for the file we are adding fixity info for. @param [String] fixityAlgorithm a valid fixity algorithm for this site (see Config.fixity_algorithms). @param [String] fixityDigest the digest value of the file, using the provided fixityAlgorithm. @return [Hash] fixity block for the object.

# File lib/ocfl_tools/ocfl_object.rb, line 255
def update_fixity(digest, fixityAlgorithm, fixityDigest)
  # Does Digest exist in @manifest? Fail if not.
  # Doe fixityAlgorithm exist as a key in @fixity? Add if not.
  unless @manifest.key?(digest) == true
    raise OcflTools::Errors::RequestedKeyNotFound, "Unable to find digest #{digest} in manifest!"
  end

  filepaths = @manifest[digest]

  # Construct the nested hash, if necessary.
  @fixity[fixityAlgorithm] = {} if @fixity.key?(fixityAlgorithm) != true

  if @fixity[fixityAlgorithm].key?(fixityDigest) != true
    @fixity[fixityAlgorithm][fixityDigest] = []
  end

  # Append the filepath to the appropriate fixityDigest, if it's not already there.
  filepaths.each do |filepath|
    if @fixity[fixityAlgorithm][fixityDigest].include?(filepath)
      next # don't add it if the filepath is already in the array.
    end

    @fixity[fixityAlgorithm][fixityDigest] = (@fixity[fixityAlgorithm][fixityDigest] << filepath)
  end
  @fixity
end
update_manifest(file, digest, version) click to toggle source

Add a file and digest to the manifest at the given version. @param [Pathname] file filepath to add to the manifest. @param [String] digest of file being added to the manifest. @param [Integer] version version of the OCFL object that the file is being added to. @note internal API.

# File lib/ocfl_tools/ocfl_object.rb, line 236
def update_manifest(file, digest, version)
  # We only ever add to the manifest.
  physical_filepath = "#{OcflTools::Utils.version_int_to_string(version)}/#{@contentDirectory}/#{file}"

  if @manifest.key?(digest)
    # This bitstream is already in the manifest.
    # We need to append the new filepath to the existing array.
    @manifest[digest] = (@manifest[digest] << physical_filepath)
    return @manifest[digest]
  end
  @manifest[digest] = [physical_filepath] # otherwise, add our first entry to the array.
  @manifest[digest]
end
version_id_list() click to toggle source

Gets an array of integers comprising all versions of this OCFL object. It is not guaranteed to be in numeric order. @return [Array{Integer}] versions that exist in the object.

# File lib/ocfl_tools/ocfl_object.rb, line 123
def version_id_list
  my_versions = []
  @versions.keys.each do |key|
    my_versions << OcflTools::Utils.version_string_to_int(key)
  end
  my_versions
end