Class: OcflTools::OcflObject

Inherits:
Object
  • Object
show all
Defined in:
lib/ocfl_tools/ocfl_object.rb

Overview

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

Direct Known Subclasses

OcflInventory

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeOcflObject

Returns a new instance of OcflObject.



30
31
32
33
34
35
36
37
38
39
40
# 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

Instance Attribute Details

#contentDirectoryString

Returns the name of the directory, inside each version directory, that the OCFL object should use as the base directory for files.

Returns:

  • (String)

    the name of the directory, inside each version directory, that the OCFL object should use as the base directory for files.



28
29
30
# File 'lib/ocfl_tools/ocfl_object.rb', line 28

def contentDirectory
  @contentDirectory
end

#digestAlgorithmString

Returns algorithm used by the OCFL object to generate digests for file manifests and versions.

Returns:

  • (String)

    algorithm used by the OCFL object to generate digests for file manifests and versions.



19
20
21
# File 'lib/ocfl_tools/ocfl_object.rb', line 19

def digestAlgorithm
  @digestAlgorithm
end

#fixityHash

Returns fixity block of the OCFL object.

Returns:

  • (Hash)

    fixity block of the OCFL object.



13
14
15
# File 'lib/ocfl_tools/ocfl_object.rb', line 13

def fixity
  @fixity
end

#headString

Returns the most recent version of the OCFL object, expressed as a string that conforms to the format defined in version_format.

Returns:

  • (String)

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



22
23
24
# File 'lib/ocfl_tools/ocfl_object.rb', line 22

def head
  @head
end

#idString

Returns id the unique identifer of the OCFL object, as defined by the local repository system.

Returns:

  • (String)

    id the unique identifer of the OCFL object, as defined by the local repository system.



16
17
18
# File 'lib/ocfl_tools/ocfl_object.rb', line 16

def id
  @id
end

#manifestHash

Returns manifest block of the OCFL object.

Returns:

  • (Hash)

    manifest block of the OCFL object.



7
8
9
# File 'lib/ocfl_tools/ocfl_object.rb', line 7

def manifest
  @manifest
end

#typeString

Returns the version of the OCFL spec to which this object conforms, expressed as a URL, as required by the OCFL specification.

Returns:

  • (String)

    the version of the OCFL spec to which this object conforms, expressed as a URL, as required by the OCFL specification.



25
26
27
# File 'lib/ocfl_tools/ocfl_object.rb', line 25

def type
  @type
end

#versionsHash

Returns versions block of the OCFL object.

Returns:

  • (Hash)

    versions block of the OCFL object.



10
11
12
# File 'lib/ocfl_tools/ocfl_object.rb', line 10

def versions
  @versions
end

Instance Method Details

#add_file(file, digest, version) ⇒ Hash

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.

Adds a file to a version.

Parameters:

  • file (Pathname)

    is the logical filename within the object.

  • digest (String)

    of filename, presumably computed with the #digestAlgorithm for the object.

  • version (Integer)

    to add file to.

Returns:

  • (Hash)

    state block reflecting the version after the changes.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 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) ⇒ Hash

Note:

Raises an error if source_file does not exist in this version.

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.

Parameters:

  • source_file (Filepath)

    filepath of source file.

  • destination_file (Filepath)

    filepath of destination file.

  • version (Integer)

    version of OCFL object.

Returns:

  • (Hash)

    state block of version after file copy has completed.



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# 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_hashHash

Note:

internal API

Returns a version hash with the correct keys created, ready for content to be added.

Returns:

  • (Hash)

    empty version Hash with ‘created’, ‘message’, ‘user’ and ‘state’ keys.



400
401
402
403
404
405
406
407
408
409
410
# 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) ⇒ Hash

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.

Parameters:

  • file (Pathname)

    logical path of file to be deleted.

  • version (Integer)

    version of object to delete file from.

Returns:

  • (Hash)

    state of version after delete has completed.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# 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_filesHash

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.

Returns:

  • (Hash)

    of files from most recent version, with logical file as key, associated physical filepath as value.



171
172
173
# 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) ⇒ String

Note:

Will raise an exception if requested filepath is not in given version.

When given a file path and version, return the associated digest from version state.

Parameters:

  • file (Pathname)

    filepath of file to return digest for.

  • version (Integer)

    version of OCFL object to search for the requested file.

Returns:

  • (String)

    digest of requested file.



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# 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) ⇒ Hash

Gets a hash of all logical files and their associated physical filepaths with the given version.

Parameters:

  • version (Integer)

    from which to generate file list.

Returns:

  • (Hash)

    of files, with logical file as key, physical location within object dir as value.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# 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) ⇒ Hash

Note:

Creates new version and copies previous versions’ state block over if requested version does not yet exist.

Gets the state block of a given version, comprising of digest keys and an array of filenames associated with those digests.

Parameters:

  • version (Integer)

    of OCFL object to retreive version state block of.

Returns:

  • (Hash)

    of digests and array of pathnames associated with this version.



135
136
137
138
# 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) ⇒ Hash

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.

Gets the existing version hash for the requested version, or else creates and populates a new, empty version hash.

Parameters:

  • version (Integer)

Returns:

  • (Hash)

    version block, if it exists, or creates new with prior version state in it.



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# 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) ⇒ String

Note:

will raise an exception if you attempt to query a non-existent version.

returns the created field for a given version.

Parameters:

  • version (Integer)

    of OCFL object to get value for.

Returns:

  • (String)

    created value set for the given version, if any.



89
90
91
92
93
94
95
# 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) ⇒ String

Note:

will raise an exception if you attempt to query a non-existent version.

returns the message field for a given version.

Parameters:

  • version (Integer)

    of OCFL object to get the message for.

Returns:

  • (String)

    message set for the given version, if any.



65
66
67
68
69
70
71
# 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) ⇒ Hash

Note:

will raise an exception if you attempt to query a nonexistent version.

Gets the user Hash for a given version. @ param [Integer] version of OCFL object to retrieve user block for.

Returns:

  • (Hash)

    user block for this version, a hash consisting of two keys, ‘name’ and ‘address’.



113
114
115
116
117
118
119
# 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) ⇒ Hash

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.

Moves (renames) a file from one location to another within the same version.

Parameters:

  • old_file (Pathname)

    filepath to move.

  • new_file (Pathname)

    new filepath.

Returns:

  • (Hash)

    state block of version after file copy has completed.



345
346
347
348
349
# 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) ⇒ @head

sets @head in current string format, when given integer.

Parameters:

  • version (Integer)

    to set head to.

Returns:

  • (@head)

    value of most recent version.



45
46
47
# 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) ⇒ Object

Note:

It is prefered to update version state via add/update/delete/copy/move file operations.

Sets the state block for a given version when provided with a hash of digest keys and an array of associated filenames.

Parameters:

  • version (Integer)

    of object to set state for.

  • hash (Hash)

    of digests (keys) and an array of pathnames (values) associated with those digests.



144
145
146
147
# 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) ⇒ Object

When given a correctly-constructed hash, create a new OCFL version. See #create_version_hash for more context.

Parameters:

  • version (Integer)

    create a new OCFL version block with this version number.

  • hash (Hash)

    use this hash for the content of the new OCFL version block.



415
416
417
418
419
420
421
422
423
424
425
426
427
# 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) ⇒ Object

Note:

will raise an exception if you attempt to query a non-existent version.

sets the created field for a given version.

Parameters:

  • version (Integer)

    of OCFL object to set value for.

  • created (String)

    value to set for given version.



77
78
79
80
81
82
83
# 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) ⇒ Object

Note:

will raise an exception if you attempt to query a non-existent version.

sets the message field for a given version.

Parameters:

  • version (Integer)

    of OCFL object to set message for.

  • message (String)

    to set for given version.



53
54
55
56
57
58
59
# 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) ⇒ Object

Note:

will raise an exception if you attempt to query a nonexistent version.

Sets the user Hash for a given version. Expects a complete User hash (with sub-keys of name & address).

Parameters:

  • version (Integer)

    of OCFL object to set the user block for.

  • user (Hash)

    block to set for this version. Must be a hash with two keys ‘name’ and ‘address’.



101
102
103
104
105
106
107
# 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) ⇒ Object

Note:

this method explicitly deletes the prior file if found, and re-creates it with a new digest via the #add_file method.

Updates an existing file with a new bitstream and digest.

Parameters:

  • file (String)

    filepath to update.

  • digest (String)

    of updated file.

  • version (Integer)

    of object to update.



222
223
224
225
226
227
228
229
# 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) ⇒ Hash

Given a digest, fixityAlgo and fixityDigest, add to fixity block.

Parameters:

  • digest (String)

    value from Manifest for the file we are adding fixity info for.

  • fixityAlgorithm (String)

    a valid fixity algorithm for this site (see Config.fixity_algorithms).

  • fixityDigest (String)

    the digest value of the file, using the provided fixityAlgorithm.

Returns:

  • (Hash)

    fixity block for the object.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# 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) ⇒ Object

Note:

internal API.

Add a file and digest to the manifest at the given version.

Parameters:

  • file (Pathname)

    filepath to add to the manifest.

  • digest (String)

    of file being added to the manifest.

  • version (Integer)

    version of the OCFL object that the file is being added to.



236
237
238
239
240
241
242
243
244
245
246
247
248
# 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_listArray{Integer}

Gets an array of integers comprising all versions of this OCFL object. It is not guaranteed to be in numeric order.

Returns:

  • (Array{Integer})

    versions that exist in the object.



123
124
125
126
127
128
129
# 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