diff --git a/app/actors/hyrax/actors/file_actor.rb b/app/actors/hyrax/actors/file_actor.rb index a142ac93fd..b192264077 100644 --- a/app/actors/hyrax/actors/file_actor.rb +++ b/app/actors/hyrax/actors/file_actor.rb @@ -1,3 +1,5 @@ +require 'wings/services/file_node_builder' + module Hyrax module Actors # Actions for a file identified by file_set and relation (maps to use predicate) @@ -8,10 +10,11 @@ class FileActor # @param [FileSet] file_set the parent FileSet # @param [Symbol, #to_sym] relation the type/use for the file # @param [User] user the user to record as the Agent acting upon the file - def initialize(file_set, relation, user) + def initialize(file_set, relation, user, use_valkyrie: false) @file_set = file_set - @relation = relation.to_sym + @relation = normalize_relation(relation, use_valkyrie: use_valkyrie) @user = user + @use_valkyrie = use_valkyrie end # Persists file as part of file_set and spawns async job to characterize and create derivatives. @@ -21,16 +24,7 @@ def initialize(file_set, relation, user) # @see IngestJob # @todo create a job to monitor the temp directory (or in a multi-worker system, directories!) to prune old files that have made it into the repo def ingest_file(io) - # Skip versioning because versions will be minted by VersionCommitter as necessary during save_characterize_and_record_committer. - Hydra::Works::AddFileToFileSet.call(file_set, - io, - relation, - versioning: false) - return false unless file_set.save - repository_file = related_file - Hyrax::VersioningService.create(repository_file, user) - pathhint = io.uploaded_file.uploader.path if io.uploaded_file # in case next worker is on same filesystem - CharacterizeJob.perform_later(file_set, repository_file.id, pathhint || io.path) + perform_ingest_file(io, use_valkyrie: @use_valkyrie) end # Reverts file and spawns async job to characterize and create derivatives. @@ -55,7 +49,72 @@ def ==(other) # @return [Hydra::PCDM::File] the file referenced by relation def related_file - file_set.public_send(relation) || raise("No #{relation} returned for FileSet #{file_set.id}") + file_set.public_send(normalize_relation(relation)) || raise("No #{relation} returned for FileSet #{file_set.id}") + end + + # Persists file as part of file_set and records a new version. + # Also spawns an async job to characterize and create derivatives. + # @param [JobIoWrapper] io the file to save in the repository, with mime_type and original_name + # @return [FileNode, FalseClass] the created file node on success, false on failure + # @todo create a job to monitor the temp directory (or in a multi-worker system, directories!) to prune old files that have made it into the repo + def perform_ingest_file(io, use_valkyrie: false) + use_valkyrie ? perform_ingest_file_through_valkyrie(io) : perform_ingest_file_through_active_fedora(io) + end + + def perform_ingest_file_through_active_fedora(io) + # Skip versioning because versions will be minted by VersionCommitter as necessary during save_characterize_and_record_committer. + Hydra::Works::AddFileToFileSet.call(file_set, + io, + relation, + versioning: false) + return false unless file_set.save + repository_file = related_file + Hyrax::VersioningService.create(repository_file, user) + pathhint = io.uploaded_file.uploader.path if io.uploaded_file # in case next worker is on same filesystem + CharacterizeJob.perform_later(file_set, repository_file.id, pathhint || io.path) + end + + def perform_ingest_file_through_valkyrie(io) + # Skip versioning because versions will be minted by VersionCommitter as necessary during save_characterize_and_record_committer. + storage_adapter = Valkyrie.config.storage_adapter + persister = Valkyrie.config.metadata_adapter.persister # TODO: Explore why valkyrie6 branch used indexing_persister adapter for this + node_builder = Wings::FileNodeBuilder.new(storage_adapter: storage_adapter, + persister: persister) + unsaved_node = io.to_file_node + unsaved_node.use = relation + begin + saved_node = node_builder.create(file: io.file, node: unsaved_node, file_set: file_set) + rescue StandardError => e # Handle error persisting file node + Rails.logger.error("Failed to save file_node through valkyrie: #{e.message}") + return false + end + Hyrax::VersioningService.create(saved_node, user) + saved_node + end + + def normalize_relation(relation, use_valkyrie: false) + use_valkyrie ? normalize_relation_for_valkyrie(relation) : normalize_relation_for_active_fedora(relation) + end + + def normalize_relation_for_active_fedora(relation) + return relation if relation.is_a? Symbol + return relation.to_sym if relation.respond_to? :to_sym + + # TODO: whereever these are set, they should use Valkyrie::Vocab::PCDMUse... making the casecmp unnecessary + return :original_file if relation.to_s.casecmp(Valkyrie::Vocab::PCDMUse.original_file.to_s) + return :extracted_file if relation.to_s.casecmp(Valkyrie::Vocab::PCDMUse.extracted_file.to_s) + return :thumbnail_file if relation.to_s.casecmp(Valkyrie::Vocab::PCDMUse.thumbnail_file.to_s) + :original_file # TODO: This should never happen. What should be done if none of the other conditions are met? + end + + def normalize_relation_for_valkyrie(relation) + return relation if relation.is_a? RDF::URI + + relation = relation.to_sym + return Valkyrie::Vocab::PCDMUse.original_file if relation == :original_file + return Valkyrie::Vocab::PCDMUse.extracted_file if relation == :extracted_file + return Valkyrie::Vocab::PCDMUse.thumbnail_file if relation == :thumbnail_file + Valkyrie::Vocab::PCDMUse.original_file # TODO: This should never happen. What should be done if none of the other conditions are met? end end end diff --git a/app/models/job_io_wrapper.rb b/app/models/job_io_wrapper.rb index f1152d212d..df0d7fd2a0 100644 --- a/app/models/job_io_wrapper.rb +++ b/app/models/job_io_wrapper.rb @@ -1,3 +1,6 @@ +require 'wings/models/file_node' +require 'wings/valkyrie/query_service' + # Primarily for jobs like IngestJob to revivify an equivalent FileActor to one that existed on # the caller's side of an asynchronous Job invocation. This involves providing slots # for the metadata that might travel w/ the actor's various supported types of @file. @@ -56,18 +59,36 @@ def mime_type super || extracted_mime_type end - def file_set - FileSet.find(file_set_id) + def file_set(use_valkyrie: false) + return FileSet.find(file_set_id) unless use_valkyrie + adapter = Valkyrie.config.metadata_adapter + query_service = Wings::Valkyrie::QueryService.new(adapter: adapter) + query_service.find_by(id: Valkyrie::ID.new(file_set_id)) + # TODO: At least temporarily, should this return the valkyrie resource version of the fileset or the active fedora fileset? end def file_actor Hyrax::Actors::FileActor.new(file_set, relation.to_sym, user) end + # @return [FileNode, FalseClass] the created file node on success, false on failure def ingest_file file_actor.ingest_file(self) end + def to_file_node + Wings::FileNode.new(label: original_name, + original_filename: original_name, + mime_type: mime_type, + use: [Valkyrie::Vocab::PCDMUse.OriginalFile]) + end + + # The magic that switches *once* between local filepath and CarrierWave file + # @return [File, StringIO, #read] File-like object ready to #read + def file + @file ||= (file_from_path || file_from_uploaded_file!) + end + private def extracted_original_name @@ -80,12 +101,6 @@ def extracted_mime_type uploaded_file ? uploaded_file.uploader.content_type : Hydra::PCDM::GetMimeTypeForFile.call(original_name) end - # The magic that switches *once* between local filepath and CarrierWave file - # @return [File, StringIO, #read] File-like object ready to #read - def file - @file ||= (file_from_path || file_from_uploaded_file!) - end - # @return [File, StringIO] depending on CarrierWave configuration # @raise when uploaded_file *becomes* required but is missing def file_from_uploaded_file! diff --git a/app/services/hyrax/versioning_service.rb b/app/services/hyrax/versioning_service.rb index 6fd1c5f0d9..0d8e36c271 100644 --- a/app/services/hyrax/versioning_service.rb +++ b/app/services/hyrax/versioning_service.rb @@ -1,26 +1,69 @@ +require 'wings/models/file_node' +require 'wings/services/file_node_builder' + module Hyrax class VersioningService - # Make a version and record the version committer - # @param [ActiveFedora::File] content - # @param [User, String] user - def self.create(content, user = nil) - content.create_version - record_committer(content, user) if user - end + class << self + # Make a version and record the version committer + # @param [ActiveFedora::File | Wings::FileNode] content + # @param [User, String] user + def create(content, user = nil) + use_valkyrie = content.is_a? Wings::FileNode + perform_create(content, user, use_valkyrie) + end - # @param [ActiveFedora::File] file - def self.latest_version_of(file) - file.versions.last - end + # @param [ActiveFedora::File | Wings::FileNode] content + def latest_version_of(file) + file.versions.last + end + + # Record the version committer of the last version + # @param [ActiveFedora::File | Wings::FileNode] content + # @param [User, String] user_key + def record_committer(content, user_key) + user_key = user_key.user_key if user_key.respond_to?(:user_key) + version = latest_version_of(content) + return if version.nil? + version_id = content.is_a?(Wings::FileNode) ? version.id.to_s : version.uri + Hyrax::VersionCommitter.create(version_id: version_id, committer_login: user_key) + end + + # TODO: Copied from valkyrie6 branch. Need to explore whether this is needed? + # # @param [FileSet] file_set + # # @param [Wings::FileNode] content + # # @param [String] revision_id + # # @param [User, String] user + # def restore_version(file_set, content, revision_id, user = nil) + # found_version = content.versions.find { |x| x.label == Array.wrap(revision_id) } + # return unless found_version + # node = Wings::FileNodeBuilder.new(storage_adapter: nil, persister: indexing_adapter.persister).attach_file_node(node: found_version, file_set: file_set) + # create(node, user) + # end + + private + + # # TODO: Should we create and use indexing adapter for persistence? This is what was used in branch valkyrie6. + # def indexing_adapter + # Valkyrie::MetadataAdapter.find(:indexing_persister) + # end + + def perform_create(content, user, use_valkyrie) + use_valkyrie ? perform_create_through_valkyrie(content, user) : perform_create_through_active_fedora(content, user) + end + + def perform_create_through_active_fedora(content, user) + content.create_version + record_committer(content, user) if user + end - # Record the version committer of the last version - # @param [ActiveFedora::File] content - # @param [User, String] user_key - def self.record_committer(content, user_key) - user_key = user_key.user_key if user_key.respond_to?(:user_key) - version = latest_version_of(content) - return if version.nil? - VersionCommitter.create(version_id: version.uri, committer_login: user_key) + def perform_create_through_valkyrie(content, user) + new_version = content.new(id: nil) + new_version.label = "version#{content.member_ids.length + 1}" + # new_version = indexing_adapter.persister.save(resource: new_version) + content.member_ids = content.member_ids + [new_version.id] + content = indexing_adapter.persister.save(resource: content) + record_committer(content, user) if user + end end end end diff --git a/lib/wings.rb b/lib/wings.rb index c9a2946080..588af8338b 100644 --- a/lib/wings.rb +++ b/lib/wings.rb @@ -36,9 +36,11 @@ module Wings; end Valkyrie::MetadataAdapter.register( Wings::Valkyrie::MetadataAdapter.new, :wings_adapter ) +Valkyrie.config.metadata_adapter = :wings_adapter Valkyrie::StorageAdapter.register( Valkyrie::Storage::Fedora .new(connection: Ldp::Client.new(ActiveFedora.fedora.host)), :fedora ) +Valkyrie.config.storage_adapter = :fedora diff --git a/lib/wings/models/file_node.rb b/lib/wings/models/file_node.rb new file mode 100644 index 0000000000..622b720f04 --- /dev/null +++ b/lib/wings/models/file_node.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Wings + class FileNode < ::Valkyrie::Resource + # TODO: Branch valkyrie6 included the valkyrie resource access controls. Including this now causes an exception. + # Need to explore whether this line should be uncommented. + # include ::Valkyrie::Resource::AccessControls + attribute :id, ::Valkyrie::Types::ID.optional + attribute :label, ::Valkyrie::Types::Set + attribute :mime_type, ::Valkyrie::Types::Set + attribute :format_label, ::Valkyrie::Types::Set # e.g. "JPEG Image" + attribute :height, ::Valkyrie::Types::Set + attribute :width, ::Valkyrie::Types::Set + attribute :checksum, ::Valkyrie::Types::Set + attribute :size, ::Valkyrie::Types::Set + attribute :original_filename, ::Valkyrie::Types::Set + attribute :file_identifiers, ::Valkyrie::Types::Set + attribute :use, ::Valkyrie::Types::Set + attribute :member_ids, ::Valkyrie::Types::Set + + # @param [ActionDispatch::Http::UploadedFile] file + def self.for(file:) + new(label: file.original_filename, + original_filename: file.original_filename, + mime_type: file.content_type, + use: file.try(:use) || [::Valkyrie::Vocab::PCDMUse.OriginalFile]) + end + + def original_file? + use.include?(::Valkyrie::Vocab::PCDMUse.OriginalFile) + end + + def thumbnail_file? + use.include?(::Valkyrie::Vocab::PCDMUse.ThumbnailImage) + end + + def extracted_file? + use.include?(::Valkyrie::Vocab::PCDMUse.ExtractedImage) + end + + def title + label + end + + def download_id + id + end + + # @return [Boolean] whether this instance is a Wings::FileNode. + def file_node? + true + end + + # @return [Boolean] whether this instance is a Hydra::Works FileSet. + def file_set? + false + end + + # @return [Boolean] whether this instance is a Hydra::Works Generic Work. + def work? + false + end + + # @return [Boolean] whether this instance is a Hydra::Works Collection. + def collection? + false + end + + def valid? + file.valid?(size: size.first, digests: { sha256: checksum.first.sha256 }) + end + + def file + ::Valkyrie::StorageAdapter.find_by(id: file_identifiers.first) + end + + def versions + query_service = Wings::Valkyrie::QueryService.new(adapter: ::Valkyrie.config.metadata_adapter) + query_service.find_members(resource: self, model: Wings::FileNode).to_a + end + end +end diff --git a/lib/wings/models/multi_checksum.rb b/lib/wings/models/multi_checksum.rb new file mode 100644 index 0000000000..a94b490d21 --- /dev/null +++ b/lib/wings/models/multi_checksum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Wings + class MultiChecksum < ::Valkyrie::Resource + attribute :sha256, ::Valkyrie::Types::SingleValuedString + attribute :md5, ::Valkyrie::Types::SingleValuedString + attribute :sha1, ::Valkyrie::Types::SingleValuedString + + def self.for(file_object) + digests = file_object.checksum(digests: [::Digest::MD5.new, ::Digest::SHA256.new, ::Digest::SHA1.new]) + MultiChecksum.new( + md5: digests.shift, + sha256: digests.shift, + sha1: digests.shift + ) + end + end +end diff --git a/lib/wings/services/file_node_builder.rb b/lib/wings/services/file_node_builder.rb new file mode 100644 index 0000000000..6e26e67f89 --- /dev/null +++ b/lib/wings/services/file_node_builder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Wings + # Stores a file and an associated FileNode + class FileNodeBuilder + attr_reader :storage_adapter, :persister + def initialize(storage_adapter:, persister:) + @storage_adapter = storage_adapter + @persister = persister + end + + # @param file [ActionDispatch::Http::UploadedFile] + # @param node [FileNode] the metadata to represent the file + # @param file_set [FileNode] the associated FileSet + # @return [FileNode] the persisted metadata node that represents the file + def create(file:, node:, file_set:) + stored_file = storage_adapter.upload(file: file, + original_filename: node.original_filename.first, + resource: node) + node.file_identifiers = [stored_file.id] + attach_file_node(node: node, file_set: file_set) + end + + def attach_file_node(node:, file_set:) + existing_node = file_set.original_file || node + node = existing_node.new(node.to_h.except(:id, :member_ids)) + saved_node = persister.save(resource: node) + file_set.file_ids = [saved_node.id] + persister.save(resource: file_set) + CharacterizeJob.perform_later(saved_node.id.to_s) + # note the returned saved_node does not yet contain the characterization done in the async job + saved_node + end + end +end diff --git a/lib/wings/valkyrie/persister.rb b/lib/wings/valkyrie/persister.rb index bb1dd4f380..aec8093a73 100644 --- a/lib/wings/valkyrie/persister.rb +++ b/lib/wings/valkyrie/persister.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'wings/models/file_node' module Wings module Valkyrie @@ -17,6 +18,7 @@ def initialize(adapter:) # @param [Valkyrie::Resource] resource # @return [Valkyrie::Resource] the persisted/updated resource def save(resource:) + return save_file(file_node: resource) if resource.is_a? Wings::FileNode af_object = resource_factory.from_resource(resource: resource) af_object.save! resource_factory.to_resource(object: af_object) @@ -24,6 +26,10 @@ def save(resource:) raise FailedSaveError.new(err.message, obj: af_object) end + def save_file(file_node:) + # TODO: SKIP for now + end + # Persists a resource using ActiveFedora # @param [Valkyrie::Resource] resource # @return [Valkyrie::Resource] the persisted/updated resource diff --git a/spec/actors/hyrax/actors/file_actor_spec.rb b/spec/actors/hyrax/actors/file_actor_spec.rb index 4d43188803..c66ff43d8c 100644 --- a/spec/actors/hyrax/actors/file_actor_spec.rb +++ b/spec/actors/hyrax/actors/file_actor_spec.rb @@ -1,12 +1,16 @@ +require 'wings/models/file_node' +require 'wings/services/file_node_builder' +require 'wings/valkyrie/query_service' + RSpec.describe Hyrax::Actors::FileActor do include ActionDispatch::TestProcess include Hyrax::FactoryHelpers - let(:user) { create(:user) } - let(:file_set) { create(:file_set) } - let(:relation) { :original_file } - let(:actor) { described_class.new(file_set, relation, user) } - let(:fixture) { fixture_file_upload('/world.png', 'image/png') } + let(:user) { create(:user) } + let(:file_set) { create(:file_set) } + let(:relation) { :original_file } + let(:actor) { described_class.new(file_set, relation, user) } + let(:fixture) { fixture_file_upload('/world.png', 'image/png') } let(:huf) { Hyrax::UploadedFile.new(user: user, file_set_uri: file_set.uri, file: fixture) } let(:io) { JobIoWrapper.new(file_set_id: file_set.id, user: user, uploaded_file: huf) } let(:pcdmfile) do @@ -17,109 +21,208 @@ end end - context 'relation' do - let(:relation) { :remastered } - let(:file_set) do - FileSetWithExtras.create!(attributes_for(:file_set)) do |file| - file.apply_depositor_metadata(user.user_key) + context 'when using active fedora directly' do + context 'relation' do + let(:relation) { :remastered } + let(:file_set) do + FileSetWithExtras.create!(attributes_for(:file_set)) do |file| + file.apply_depositor_metadata(user.user_key) + end end - end - before do - class FileSetWithExtras < FileSet - directly_contains_one :remastered, through: :files, type: ::RDF::URI('http://pcdm.org/use#IntermediateFile'), class_name: 'Hydra::PCDM::File' + before do + class FileSetWithExtras < FileSet + directly_contains_one :remastered, through: :files, type: ::RDF::URI('http://pcdm.org/use#IntermediateFile'), class_name: 'Hydra::PCDM::File' + end + end + after do + Object.send(:remove_const, :FileSetWithExtras) + end + it 'uses the relation from the actor' do + expect(CharacterizeJob).to receive(:perform_later).with(FileSetWithExtras, String, huf.uploader.path) + actor.ingest_file(io) + expect(file_set.reload.remastered.mime_type).to eq 'image/png' end end - after do - Object.send(:remove_const, :FileSetWithExtras) - end - it 'uses the relation from the actor' do - expect(CharacterizeJob).to receive(:perform_later).with(FileSetWithExtras, String, huf.uploader.path) + + it 'uses the provided mime_type' do + allow(fixture).to receive(:content_type).and_return('image/gif') + expect(CharacterizeJob).to receive(:perform_later).with(FileSet, String, huf.uploader.path) actor.ingest_file(io) - expect(file_set.reload.remastered.mime_type).to eq 'image/png' + expect(file_set.reload.original_file.mime_type).to eq 'image/gif' end - end - it 'uses the provided mime_type' do - allow(fixture).to receive(:content_type).and_return('image/gif') - expect(CharacterizeJob).to receive(:perform_later).with(FileSet, String, huf.uploader.path) - actor.ingest_file(io) - expect(file_set.reload.original_file.mime_type).to eq 'image/gif' - end + context 'with two existing versions from different users' do + let(:fixture2) { fixture_file_upload('/small_file.txt', 'text/plain') } + let(:huf2) { Hyrax::UploadedFile.new(user: user2, file_set_uri: file_set.uri, file: fixture2) } + let(:io2) { JobIoWrapper.new(file_set_id: file_set.id, user: user2, uploaded_file: huf2) } + let(:user2) { create(:user) } + let(:actor2) { described_class.new(file_set, relation, user2) } + let(:versions) { file_set.reload.original_file.versions } - context 'with two existing versions from different users' do - let(:fixture2) { fixture_file_upload('/small_file.txt', 'text/plain') } - let(:huf2) { Hyrax::UploadedFile.new(user: user2, file_set_uri: file_set.uri, file: fixture2) } - let(:io2) { JobIoWrapper.new(file_set_id: file_set.id, user: user2, uploaded_file: huf2) } - let(:user2) { create(:user) } - let(:actor2) { described_class.new(file_set, relation, user2) } - let(:versions) { file_set.reload.original_file.versions } + before do + allow(Hydra::Works::CharacterizationService).to receive(:run).with(any_args) + actor.ingest_file(io) + actor2.ingest_file(io2) + end - before do - allow(Hydra::Works::CharacterizationService).to receive(:run).with(any_args) - actor.ingest_file(io) - actor2.ingest_file(io2) + it 'has two versions' do + expect(versions.all.count).to eq 2 + # the current version + expect(Hyrax::VersioningService.latest_version_of(file_set.reload.original_file).label).to eq 'version2' + expect(file_set.original_file.mime_type).to eq 'text/plain' + expect(file_set.original_file.original_name).to eq 'small_file.txt' + expect(file_set.original_file.content).to eq fixture2.open.read + # the user for each version + expect(Hyrax::VersionCommitter.where(version_id: versions.first.uri).pluck(:committer_login)).to eq [user.user_key] + expect(Hyrax::VersionCommitter.where(version_id: versions.last.uri).pluck(:committer_login)).to eq [user2.user_key] + end end - it 'has two versions' do - expect(versions.all.count).to eq 2 - # the current version - expect(Hyrax::VersioningService.latest_version_of(file_set.reload.original_file).label).to eq 'version2' - expect(file_set.original_file.mime_type).to eq 'text/plain' - expect(file_set.original_file.original_name).to eq 'small_file.txt' - expect(file_set.original_file.content).to eq fixture2.open.read - # the user for each version - expect(Hyrax::VersionCommitter.where(version_id: versions.first.uri).pluck(:committer_login)).to eq [user.user_key] - expect(Hyrax::VersionCommitter.where(version_id: versions.last.uri).pluck(:committer_login)).to eq [user2.user_key] + describe '#ingest_file' do + before do + expect(Hydra::Works::AddFileToFileSet).to receive(:call).with(file_set, io, relation, versioning: false) + end + it 'when the file is available' do + allow(file_set).to receive(:save).and_return(true) + allow(file_set).to receive(relation).and_return(pcdmfile) + expect(Hyrax::VersioningService).to receive(:create).with(pcdmfile, user) + expect(CharacterizeJob).to receive(:perform_later).with(FileSet, pcdmfile.id, huf.uploader.path) + actor.ingest_file(io) + end + it 'returns false when save fails' do + allow(file_set).to receive(:save).and_return(false) + expect(actor.ingest_file(io)).to be_falsey + end end - end - describe '#ingest_file' do - before do - expect(Hydra::Works::AddFileToFileSet).to receive(:call).with(file_set, io, relation, versioning: false) - end - it 'when the file is available' do - allow(file_set).to receive(:save).and_return(true) - allow(file_set).to receive(relation).and_return(pcdmfile) - expect(Hyrax::VersioningService).to receive(:create).with(pcdmfile, user) - expect(CharacterizeJob).to receive(:perform_later).with(FileSet, pcdmfile.id, huf.uploader.path) - actor.ingest_file(io) - end - it 'returns false when save fails' do - allow(file_set).to receive(:save).and_return(false) - expect(actor.ingest_file(io)).to be_falsey + describe '#revert_to' do + let(:revision_id) { 'asdf1234' } + + before do + allow(pcdmfile).to receive(:restore_version).with(revision_id) + allow(file_set).to receive(relation).and_return(pcdmfile) + expect(Hyrax::VersioningService).to receive(:create).with(pcdmfile, user) + expect(CharacterizeJob).to receive(:perform_later).with(file_set, pcdmfile.id) + end + + it 'reverts to a previous version of a file' do + expect(file_set).not_to receive(:remastered) + expect(actor.relation).to eq(:original_file) + actor.revert_to(revision_id) + end + + describe 'for a different relation' do + let(:relation) { :remastered } + + it 'reverts to a previous version of a file' do + expect(actor.relation).to eq(:remastered) + actor.revert_to(revision_id) + end + it 'does not rely on the default relation' do + pending "Hydra::Works::VirusCheck must support other relations: https://github.com/samvera/hyrax/issues/1187" + expect(actor.relation).to eq(:remastered) + expect(file_set).not_to receive(:original_file) + actor.revert_to(revision_id) + end + end end end - describe '#revert_to' do - let(:revision_id) { 'asdf1234' } + context 'when using valkyrie' do + let(:user) { create(:user) } + let(:file_set) { create(:file_set) } + let(:relation) { Valkyrie::Vocab::PCDMUse.OriginalFile } + let(:actor) { described_class.new(file_set, relation, user, use_valkyrie: true) } + let(:fixture) { fixture_file_upload('/world.png', 'image/png') } + let(:huf) { Hyrax::UploadedFile.new(user: user, file: fixture) } + let(:io) { JobIoWrapper.new(file_set_id: file_set.id, user: user, uploaded_file: huf, path: huf.uploader.path) } + let(:storage_adapter) { Valkyrie.config.storage_adapter } + let(:metadata_adapter) { Valkyrie.config.metadata_adapter } + let(:persister) { metadata_adapter.persister } + let(:query_service) { Wings::Valkyrie::QueryService.new(adapter: metadata_adapter) } + let(:file_node) do + node_builder = Wings::FileNodeBuilder.new(storage_adapter: storage_adapter, persister: persister) + node = Wings::FileNode.for(file: fixture) + node_builder.create(file: fixture, node: node, file_set: file_set) + end - before do - allow(pcdmfile).to receive(:restore_version).with(revision_id) - allow(file_set).to receive(relation).and_return(pcdmfile) - expect(Hyrax::VersioningService).to receive(:create).with(pcdmfile, user) - expect(CharacterizeJob).to receive(:perform_later).with(file_set, pcdmfile.id) + context 'relation' do + let(:relation) { RDF::URI.new("http://pcdm.org/use#remastered") } + + it 'uses the relation from the actor' do + pending 'implementation of Wings::Valkyrie::Persister #save_file_node' + expect(Hyrax::VersioningService).to receive(:create).with(Wings::FileNode, user) + expect(CharacterizeJob).to receive(:perform_later) + saved_node = actor.ingest_file(io) + reloaded = query_service.find_by(id: file_set.id) + expect(reloaded.member_by(use: relation).id).to eq saved_node.id + end end - it 'reverts to a previous version of a file' do - expect(file_set).not_to receive(:remastered) - expect(actor.relation).to eq(:original_file) - actor.revert_to(revision_id) + it 'uses the provided mime_type' do + pending 'implementation of Wings::Valkyrie::Persister #save_file_node' + allow(fixture).to receive(:content_type).and_return('image/gif') + expect(Hyrax::VersioningService).to receive(:create).with(Wings::FileNode, user) + expect(CharacterizeJob).to receive(:perform_later) + saved_node = actor.ingest_file(io) + expect(saved_node.mime_type).to eq ['image/gif'] end - describe 'for a different relation' do - let(:relation) { :remastered } + context 'with two existing versions from different users' do + let(:fixture2) { fixture_file_upload('/small_file.txt', 'text/plain') } + let(:huf2) { Hyrax::UploadedFile.new(user: user2, file: fixture2) } + let(:io2) { JobIoWrapper.new(file_set_id: file_set.id, user: user2, uploaded_file: huf2, path: huf2.uploader.path) } + let(:user2) { create(:user) } + let(:actor2) { described_class.new(file_set, relation, user2) } + let(:adapter) { Valkyrie.config.metadata_adapter } + let(:query_service) { Wings::Valkyrie::QueryService.new(adapter: adapter) } + let(:versions) do + reloaded = query_service.find_by(id: file_set.id) + reloaded.original_file.versions + end - it 'reverts to a previous version of a file' do - expect(actor.relation).to eq(:remastered) - actor.revert_to(revision_id) + before do + # expect(Hyrax::VersioningService).to receive(:create).with(Wings::FileNode, user) + # expect(Hyrax::VersioningService).to receive(:create).with(Wings::FileNode, user2) + # expect(CharacterizeJob).to receive(:perform_later) + allow(CharacterizeJob).to receive(:perform_later) + actor.ingest_file(io) + actor2.ingest_file(io2) end - it 'does not rely on the default relation' do - pending "Hydra::Works::VirusCheck must support other relations: https://github.com/samvera/hyrax/issues/1187" - expect(actor.relation).to eq(:remastered) - expect(file_set).not_to receive(:original_file) - actor.revert_to(revision_id) + + it 'has two versions' do + pending 'implementation of Wings::Valkyrie::Persister #save_file_node' + expect(versions.count).to eq 2 + # the current version + reloaded = query_service.find_by(id: file_set.id) + expect(Hyrax::VersioningService.latest_version_of(reloaded.original_file).label).to eq ['version2'] + expect(file_set.original_file.mime_type).to eq ['text/plain'] + expect(file_set.original_file.original_filename).to eq ['small_file.txt'] + expect(file_set.original_file.file.read).to eq fixture2.open.read + # the user for each versioe + expect(Hyrax::VersionCommitter.where(version_id: versions.first.id.to_s).pluck(:committer_login)).to eq [user.user_key] + expect(Hyrax::VersionCommitter.where(version_id: versions.last.id.to_s).pluck(:committer_login)).to eq [user2.user_key] + end + end + + describe '#ingest_file' do + it 'when the file is available' do + pending 'implementation of Wings::Valkyrie::Persister #save_file_node' + expect(Hyrax::VersioningService).to receive(:create).with(Wings::FileNode, user) + expect(CharacterizeJob).to receive(:perform_later) + actor.ingest_file(io) + reloaded = query_service.find_by(id: file_set.id) + expect(reloaded.member_by(use: relation)).not_to be_nil + end + # rubocop:disable RSpec/AnyInstance + it 'returns false when save fails' do + expect(Hyrax::VersioningService).not_to receive(:create).with(Wings::FileNode, user) + expect(CharacterizeJob).not_to receive(:perform_later) + allow_any_instance_of(Wings::FileNodeBuilder).to receive(:create).and_raise(StandardError) + expect(actor.ingest_file(io)).to be_falsey end + # rubocop:enable RSpec/AnyInstance end end end diff --git a/spec/models/job_io_wrapper_spec.rb b/spec/models/job_io_wrapper_spec.rb index 50311a182c..c2489d6ae3 100644 --- a/spec/models/job_io_wrapper_spec.rb +++ b/spec/models/job_io_wrapper_spec.rb @@ -169,4 +169,21 @@ end end end + + describe '#file_set' do + context 'when finding through active fedora' do + xit 'finds the file set using active fedora and returns an instance of an active fedora file set' + end + context 'when finding through valkyrie' do + xit 'finds the file set through valkyrie and returns an instance of an active fedora file set' + end + end + + describe '#to_file_node' do + xit 'creates and returns file_node' + end + + describe '#file' do + xit 'switches between local filepath and CarrierWave file' + end end diff --git a/spec/wings/models/file_node_spec.rb b/spec/wings/models/file_node_spec.rb new file mode 100644 index 0000000000..d6ecdf9903 --- /dev/null +++ b/spec/wings/models/file_node_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true +require 'wings/models/file_node' +require 'wings/models/multi_checksum' + +RSpec.describe Wings::FileNode do + let(:adapter) { Wings::Valkyrie::MetadataAdapter.new } + let(:persister) { adapter.persister } + let(:storage_adapter) { Valkyrie.config.storage_adapter } + let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/world.png', 'image/png') } + let(:subject) do + described_class.for(file: file).new(id: 'test_id', format_label: 'test_format_label') + end + let(:uploaded_file) do + storage_adapter.upload(file: file, + original_filename: file.original_filename, + resource: subject) + end + let(:pcdm_file_uri) { RDF::URI('http://pcdm.org/models#File') } + + before do + stub_request(:get, 'http://test_id/original').to_return(status: 200, body: "", headers: {}) + + subject.file_identifiers = uploaded_file.id + subject.checksum = Wings::MultiChecksum.for(uploaded_file) + subject.size = uploaded_file.size + persister.save(resource: subject) + end + + it 'sets the proper attributes' do + expect(subject.id.to_s).to eq 'test_id' + expect(subject.label).to contain_exactly('world.png') + expect(subject.original_filename).to contain_exactly('world.png') + expect(subject.mime_type).to contain_exactly('image/png') + expect(subject.format_label).to contain_exactly('test_format_label') + expect(subject.use).to contain_exactly(Valkyrie::Vocab::PCDMUse.OriginalFile) + end + + describe '#original_file?' do + context 'when use says file is the original file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.OriginalFile, pcdm_file_uri] } + it 'returns true' do + expect(subject).to be_original_file + end + end + context 'when use does not say file is the original file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.ThumbnailImage, pcdm_file_uri] } + it 'returns false' do + expect(subject).not_to be_original_file + end + end + end + + describe '#thumbnail_file?' do + context 'when use says file is the thumbnail file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.ThumbnailImage, pcdm_file_uri] } + it 'returns true' do + expect(subject).to be_thumbnail_file + end + end + context 'when use does not say file is the thumbnail file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.OriginalFile, pcdm_file_uri] } + it 'returns false' do + expect(subject).not_to be_thumbnail_file + end + end + end + + describe '#extracted_file?' do + context 'when use says file is the extracted file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.ExtractedImage, pcdm_file_uri] } + it 'returns true' do + expect(subject).to be_extracted_file + end + end + context 'when use does not say file is the extracted file' do + before { subject.use = [Valkyrie::Vocab::PCDMUse.OriginalFile, pcdm_file_uri] } + it 'returns false' do + expect(subject).not_to be_extracted_file + end + end + end + + describe '#title' do + it 'uses the label' do + expect(subject.title).to contain_exactly('world.png') + end + end + + describe '#download_id' do + it 'uses the id' do + expect(subject.download_id.to_s).to eq 'test_id' + end + end + + describe '#file_node?' do + it 'is a file_node' do + expect(subject).to be_file_node + end + end + + describe '#file_set?' do + it 'is not a file_set' do + expect(subject).not_to be_file_set + end + end + + describe '#work?' do + it 'is not a work' do + expect(subject).not_to be_work + end + end + + describe '#collection?' do + it 'is not a collection' do + expect(subject).not_to be_collection + end + end + + describe "#valid?" do + it 'is valid' do + expect(subject).to be_valid + end + end + + describe '#file' do + it 'returns file from storage adapter' do + expect(subject.file).to be_a Valkyrie::StorageAdapter::StreamFile + end + end + + describe '#versions' do + context 'when no versions saved' do + it 'returns empty array' do + expect(subject.versions).to eq [] + end + end + context 'when versions saved' do + it 'returns a set of file_nodes for previous versions' + end + end +end