diff --git a/app/actors/file_set_attach_files_actor.rb b/app/actors/file_set_attach_files_actor.rb new file mode 100644 index 0000000..158366b --- /dev/null +++ b/app/actors/file_set_attach_files_actor.rb @@ -0,0 +1,33 @@ +# Actions are decoupled from controller logic so that they may be called from a controller or a background job. +class FileSetAttachFilesActor < Hyrax::Actors::FileSetActor + ORIGINAL_FILE = ExtendedContainedFiles::ORIGINAL_FILE + # Spawns asynchronous IngestJob + # Called from FileSetsController, AttachFilesToWorkJob, ImportURLJob, IngestLocalFileJob + # @param [Hyrax::UploadedFile, File, ActionDigest::HTTP::UploadedFile] file the file uploaded by the user + # @param [Symbol, #to_s] relation + # @return [IngestJob, FalseClass] false on failure, otherwise the queued job + def create_content(file, relation = ORIGINAL_FILE) + # If the file set doesn't have a title or label assigned, set a default. + file_set.label ||= label_for(file) + file_set.title = [file_set.label] if file_set.title.blank? + return false unless file_set.save # Need to save to get an id + IngestJob.perform_later(wrapper!(file, relation)) + end + + # Spawns asynchronous IngestJob with user notification afterward + # @param [Hyrax::UploadedFile, File, ActionDigest::HTTP::UploadedFile] file the file uploaded by the user + # @param [Symbol, #to_s] relation + # @return [IngestJob] the queued job + def update_content(file, relation = ORIGINAL_FILE) + IngestJob.perform_later(wrapper!(file, relation), notification: true) + end + + # Spawns async ImportUrlJob to attach remote file to fileset + # @param [#to_s] url + # @return [IngestUrlJob] the queued job + def import_url(url, relation = ORIGINAL_FILE) + file_set.update(import_url: url.to_s) + operation = Hyrax::Operation.create!(user: user, operation_type: "Attach File") + ImportUrlWithFileUseJob.perform_later(file_set, operation, url, relation) + end +end diff --git a/app/actors/hyrax/actors/create_with_files_actor.rb b/app/actors/hyrax/actors/create_with_files_actor.rb new file mode 100644 index 0000000..c39ec38 --- /dev/null +++ b/app/actors/hyrax/actors/create_with_files_actor.rb @@ -0,0 +1,62 @@ +module Hyrax + module Actors + # Creates a work and attaches files to the work + class CreateWithFilesActor < Hyrax::Actors::AbstractActor + # @param [Hyrax::Actors::Environment] env + # @return [Boolean] true if create was successful + def create(env) + uploaded_file_ids = filter_file_ids(env.attributes.delete(:uploaded_files)) + files = uploaded_files(uploaded_file_ids) + file_uses = file_use(env.attributes) + validate_files(files, env) && next_actor.create(env) && attach_files(files, env, file_uses) + end + + # @param [Hyrax::Actors::Environment] env + # @return [Boolean] true if update was successful + def update(env) + uploaded_file_ids = filter_file_ids(env.attributes.delete(:uploaded_files)) + files = uploaded_files(uploaded_file_ids) + file_uses = file_use(env.attributes) + validate_files(files, env) && next_actor.update(env) && attach_files(files, env, file_uses) + end + + private + + def filter_file_ids(input) + Array.wrap(input).select(&:present?) + end + + # ensure that the files we are given are owned by the depositor of the work + def validate_files(files, env) + expected_user_id = env.user.id + files.each do |file| + if file.user_id != expected_user_id + Rails.logger.error "User #{env.user.user_key} attempted to ingest file #{file.id} belongs to other users" + return false + end + end + true + end + + # @return [TrueClass] + def attach_files(files, env, file_uses) + return true unless files + AttachFilesToFileSetJob.perform_later(env.curation_concern, + files, file_uses, env.attributes.to_h.symbolize_keys) + true + end + + # Fetch uploaded_files from the database + def uploaded_files(uploaded_file_ids) + return [] if uploaded_file_ids.empty? + UploadedFile.find(uploaded_file_ids) + end + + # File use for fileset + def file_use(attributes) + return [] unless attributes.key? :file_use + attributes.delete(:file_use) + end + end + end +end diff --git a/app/actors/hyrax/actors/create_with_remote_files_actor.rb b/app/actors/hyrax/actors/create_with_remote_files_actor.rb new file mode 100644 index 0000000..bbda0ba --- /dev/null +++ b/app/actors/hyrax/actors/create_with_remote_files_actor.rb @@ -0,0 +1,104 @@ +module Hyrax + module Actors + # Attaches remote files to the work + class CreateWithRemoteFilesActor < Hyrax::Actors::AbstractActor + ORIGINAL_FILE = ExtendedContainedFiles::ORIGINAL_FILE + + # @param [Hyrax::Actors::Environment] env + # @return [Boolean] true if create was successful + def create(env) + remote_files = env.attributes.delete(:remote_files) + file_uses = file_use(env.attributes) + next_actor.create(env) && attach_files(env, remote_files, file_uses) + end + + # @param [Hyrax::Actors::Environment] env + # @return [Boolean] true if update was successful + def update(env) + remote_files = env.attributes.delete(:remote_files) + file_uses = file_use(env.attributes) + next_actor.update(env) && attach_files(env, remote_files, file_uses) + end + + private + + # @param [HashWithIndifferentAccess] remote_files + # @return [TrueClass] + def attach_files(env, remote_files, file_uses) + return true unless remote_files + + ingest_remote_files(env, remote_files, file_uses) + + true + end + + # Generic utility for creating FileSet from a URL + # Used in to import files using URLs from a file picker like browse_everything + def create_file_from_url(env, file_infos, file_uses) + fs = create_file_set(env) + file_infos.each_with_index do |file_info, index| + fs.update(import_url: file_info[:url].to_s, label: file_info[:file_name]) + uri = URI.parse(URI.encode(file_info[:url])) + if uri.scheme == 'file' + IngestLocalFileJob.perform_later(fs, URI.decode(uri.path), env.user, file_uses[index]) + else + ImportUrlWithFileUseJob.perform_later(fs, operation_for(user: env.user), uri, file_uses[index]) + end + end + end + + def operation_for(user:) + Hyrax::Operation.create!(user: user, + operation_type: "Attach Remote File") + end + + # File use for fileset + def file_use(attributes) + return [] unless attributes.key? :file_use + attributes[:file_use] + end + + def create_file_set(env) + work_permissions = env.curation_concern.permissions.map(&:to_hash) + ::FileSet.new do |fs| + actor = Hyrax::Actors::FileSetActor.new(fs, env.user) + actor.create_metadata(visibility: env.curation_concern.visibility) + actor.attach_to_work(env.curation_concern) + fs.permissions_attributes = work_permissions + fs.save! + end + end + + # Arrange and attach the remote files basing on file use provided + # @param [Hyrax::Actors::Environment] env + # @param [Array] remote_files + # @param [Array] file_uses + def ingest_remote_files(env, remote_files, file_uses) + file_set_urls = [] + file_set_file_uses = [] + remote_files.each_with_index do |file_info, index| + next if file_info.blank? || file_info[:url].blank? + + file_use = file_uses.present? && file_uses.count > index ? file_uses[index] : ORIGINAL_FILE + attach_remote_files_to_file_set(env, file_set_urls, file_set_file_uses) if file_use == ORIGINAL_FILE + file_set_file_uses << file_use + file_set_urls << file_info + end + + # Files in the last file set that need attached + attach_remote_files_to_file_set(env, file_set_urls, file_set_file_uses) + end + + # Attach files with file uses to a FileSet + # @param [Array] file_set_urls + # @param [Array] file_set_file_uses + def attach_remote_files_to_file_set(env, file_set_urls, file_set_file_uses) + # One original_file one FileSet: multiple files with different file uses will be attached to a FileSet + return unless file_set_urls.count.positive? + create_file_from_url(env, file_set_urls.select { |f| f }, file_set_file_uses.select { |u| u }) + file_set_urls.clear + file_set_file_uses.clear + end + end + end +end diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 4aeee38..248dc12 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -26,3 +26,4 @@ li > div.row {background-color: #eee;} .form-group.time_span ul.listing {list-style-type: none; padding: 0; margin: 0; max-width: 530px} .form-group.multi_value ul.listing {list-style-type: none; padding: 0; margin: 0; max-width: 535px} .object_resource_visibility_after_embargo {visibility: hidden;} +table.additional-files {padding-top: 0; margin-top: -10px;} diff --git a/app/controllers/hyrax/downloads_controller.rb b/app/controllers/hyrax/downloads_controller.rb index 92ee164..816ea3a 100644 --- a/app/controllers/hyrax/downloads_controller.rb +++ b/app/controllers/hyrax/downloads_controller.rb @@ -41,7 +41,12 @@ def derivative_download_options # Hydra::Ability#download_permissions can't be used in this case because it assumes # that files are in a LDP basic container, and thus, included in the asset's uri. def authorize_download! - authorize! :read, params[asset_param_key] + case params[:file] + when ExtendedContainedFiles::PRESERVATION_MASTER_FILE + authorize! :edit, params[asset_param_key] + else + authorize! :read, params[asset_param_key] + end rescue CanCan::AccessDenied icon = icon_path params[asset_param_key] if icon @@ -65,11 +70,14 @@ def default_image # Returns the file from the repository or a path to a file on the local file system, if it exists. def load_file file_reference = params[:file] - return default_file unless file_reference + return authorized_file_download(default_file) unless file_reference icon = icon_path(params[asset_param_key], true) if file_reference == 'thumbnail' return icon if icon + file_content = relation_content(file_reference) + return authorized_file_download(file_content) if file_content + file_path = Hyrax::DerivativePath.derivative_path_for_reference(params[asset_param_key], file_reference) File.exist?(file_path) ? file_path : nil end @@ -105,5 +113,28 @@ def icon_path(asset_id, absolute_path = false) end ::ThumbnailPathService.icon_path(VisibilityService.visibility_value(file_set.rights_override)) end + + # Retrieve the content for relation + # @param [string] relation + # return [binary] + def relation_content(relation) + case relation + when ExtendedContainedFiles::PRESERVATION_MASTER_FILE + preservation_master_file = asset.attached_files.base.preservation_master_file + return preservation_master_file if preservation_master_file + when ExtendedContainedFiles::TRANSCRIPT + return asset.attached_files.base.transcript if asset.attached_files.base.transcript + end + end + + def authorized_file_download(file_content) + return file_content unless file_content.is_a?(ActiveFedora::File) + if can?(:read, file_content) + file_content + else + # return the lower resolution service image + Hyrax::DerivativePath.derivative_path_for_reference(params[asset_param_key], 'thumbnail') + end + end end end diff --git a/app/indexers/file_set_indexer.rb b/app/indexers/file_set_indexer.rb index 20b2597..e58f3dc 100644 --- a/app/indexers/file_set_indexer.rb +++ b/app/indexers/file_set_indexer.rb @@ -2,4 +2,54 @@ class FileSetIndexer < Hyrax::FileSetIndexer self.thumbnail_path_service = ::ThumbnailPathService + + def generate_solr_document + super.tap do |solr_doc| + file_uses.each do |file_use| + file_metadata = file_metadata(object.send(file_use)) + file_metadata = file_metadata.merge(file_use: file_use) + Solrizer.insert_field(solr_doc, "files_json", file_metadata.to_json, :stored_searchable) + end + end + end + + def file_uses + [].tap do |process| + process << :original_file if object.original_file + process << :preservation_master_file if object.preservation_master_file + process << :transcript if object.transcript + end + end + + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def file_metadata(file) + {}.tap do |attrs| + attrs[:uri] = file.uri.to_s + attrs[:label] = file.label.first if file.label.present? + attrs[:file_name] = file.file_name.first + attrs[:file_format] = format(file) + attrs[:file_size] = file.file_size.present? ? file.file_size.first : file.content.size + attrs[:height] = Integer(file.height.first) if file.height.present? + attrs[:width] = Integer(file.width.first) if file.width.present? + attrs[:mime_type] = file.mime_type + attrs[:digest] = file.digest.first.to_s + attrs[:file_title] = file.file_title.first if file.file_title.present? + attrs[:duration] = file.duration.first if file.duration.present? + attrs[:sample_rate] = file.sample_rate.first if file.sample_rate.present? + attrs[:original_checksum] = file.original_checksum.first + attrs[:date_uploaded] = file.date_modified.present? ? file.date_modified.first : file.create_date + end + end + + private + + def format(file) + if file.mime_type.present? && file.format_label.present? + "#{file.mime_type.split('/').last} (#{file.format_label.join(', ')})" + elsif file.mime_type.present? + file.mime_type.split('/').last + elsif file.format_label.present? + file.format_label + end + end end diff --git a/app/jobs/attach_files_to_file_set_job.rb b/app/jobs/attach_files_to_file_set_job.rb new file mode 100644 index 0000000..c2abb5a --- /dev/null +++ b/app/jobs/attach_files_to_file_set_job.rb @@ -0,0 +1,75 @@ +# Converts UploadedFiles into FileSets and attaches them to works. +class AttachFilesToFileSetJob < AttachFilesToWorkJob + queue_as Hyrax.config.ingest_queue_name + + ORIGINAL_FILE = ExtendedContainedFiles::ORIGINAL_FILE + + # @param [ActiveFedora::Base] work - the work object + # @param [Array] uploaded_files - an array of files to attach + def perform(work, uploaded_files, file_uses, **work_attributes) + user = User.find_by_user_key(work.depositor) + + file_set_files = [] + file_set_file_uses = [] + uploaded_files.each_with_index do |uploaded_file, index| + file_use = file_uses.present? && file_uses.count > index ? file_uses[index] : ORIGINAL_FILE + + # One original_file one FileSet: multiple files with different file uses will be attached to a FileSet + attach_files(user, work, work_attributes, file_set_files, file_set_file_uses) if file_use == ORIGINAL_FILE + + file_set_file_uses << file_use + file_set_files << uploaded_file + end + + # Files in the last file set that need attached + attach_files(user, work, work_attributes, file_set_files, file_set_file_uses) + end + + private + + # @param [Hyrax::Actors::FileSetActor] actor + # @param [Hyrax::UploadedFile] uploaded_file .uploader.file must be a CarrierWave::SanitizedFile or + # .uploader.url must be present + def attach_content(actor, uploaded_file, relation = ORIGINAL_FILE) + file_uploader = uploaded_file.uploader + if file_uploader.file.is_a? CarrierWave::SanitizedFile + actor.create_content(file_uploader.file.to_file, relation) + elsif file_uploader.url.present? + actor.import_url(file_uploader.url, relation) + else + raise ArgumentError, "#{file_uploader.class} received with #{file_uploader.file.class} object and no URL" + end + end + + # Create file set + def create_file_set(user, work, work_attributes, work_permissions) + ::FileSet.new do |fs| + actor = Hyrax::Actors::FileSetActor.new(fs, user) + actor.create_metadata(visibility_attributes(work_attributes)) + actor.attach_to_work(work) + fs.permissions_attributes = work_permissions + fs.save! + end + end + + # Attached files to FileSet + # @param [::FileSet] file_set + # @param [Hyrax::UploadedFile] file_set_files + # @param [Array] file_set_file_uses + def attach_files(user, work, work_attributes, file_set_files, file_uses) + return unless file_set_files.count.positive? + + work_permissions = work.permissions.map(&:to_hash) + file_set = create_file_set(user, work, work_attributes, work_permissions) + + file_set_files.each_with_index do |uploaded_file, index| + file_use = file_uses.present? && file_uses.count > index ? file_uses[index] : ORIGINAL_FILE + + uploaded_file.update(file_set_uri: file_set.uri) + actor = FileSetAttachFilesActor.new(file_set, user) + attach_content(actor, uploaded_file, file_use) + end + file_set_files.clear + file_uses.clear + end +end diff --git a/app/jobs/import_url_with_file_use_job.rb b/app/jobs/import_url_with_file_use_job.rb new file mode 100644 index 0000000..48557d3 --- /dev/null +++ b/app/jobs/import_url_with_file_use_job.rb @@ -0,0 +1,34 @@ +require 'uri' +require 'tmpdir' +require 'browse_everything/retriever' + +# Given a FileSet that has an import_url property, +# download that file and put it into Fedora +# Called by AttachFilesToWorkJob (when files are uploaded to s3) +# and CreateWithRemoteFilesActor when files are located in some other service. +class ImportUrlWithFileUseJob < ::ImportUrlJob + queue_as Hyrax.config.ingest_queue_name + + # @param [FileSet] file_set + # @param [Hyrax::BatchCreateOperation] operation + def perform(file_set, operation, uri, relation) + operation.performing! + user = User.find_by_user_key(file_set.depositor) + + copy_remote_file(URI(uri)) do |f| + # reload the FileSet once the data is copied since this is a long running task + file_set.reload + + # FileSetActor operates synchronously so that this tempfile is available. + # If asynchronous, the job might be invoked on a machine that did not have this temp file on its file system! + # NOTE: The return status may be successful even if the content never attaches. + if FileSetAttachFilesActor.new(file_set, user).create_content(f, relation) + operation.success! + else + # send message to user on download failure + Hyrax.config.callback.run(:after_import_url_failure, file_set, user) + operation.fail!(file_set.errors.full_messages.join(' ')) + end + end + end +end diff --git a/app/jobs/ingest_local_file_job.rb b/app/jobs/ingest_local_file_job.rb index 8b4ca35..4fdde2e 100644 --- a/app/jobs/ingest_local_file_job.rb +++ b/app/jobs/ingest_local_file_job.rb @@ -4,11 +4,13 @@ class IngestLocalFileJob < Hyrax::ApplicationJob # @param [FileSet] file_set # @param [String] path # @param [User] user - def perform(file_set, path, user) + def perform(file_set, path, user, relation = ExtendedContainedFiles::ORIGINAL_FILE) file_set.label ||= File.basename(path) - actor = Hyrax::Actors::FileSetActor.new(file_set, user) - if actor.create_content(File.open(path)) + actor = FileSetAttachFilesActor.new(file_set, user) + + status = actor.create_content(File.open(path), relation) + if status # FileUtils.rm(path) Hyrax.config.callback.run(:after_import_local_file_success, file_set, user, path) else diff --git a/app/jobs/ingest_work_job.rb b/app/jobs/ingest_work_job.rb index 49430be..50a1fdd 100644 --- a/app/jobs/ingest_work_job.rb +++ b/app/jobs/ingest_work_job.rb @@ -13,7 +13,6 @@ def perform(user, model, components, log) components.each do |attributes| levels << Import::TabularImporter.const_get(attributes.delete(:level).delete('-').upcase) - work = model.constantize.new works << work current_ability = Ability.new(user) diff --git a/app/models/ability.rb b/app/models/ability.rb index 397ea3f..04e2e29 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -44,9 +44,13 @@ def curator_permissions can [:download], FileSet end - def campus_permissions; end + def campus_permissions + download_file_permission + end - def anonymous_permissions; end + def anonymous_permissions + download_file_permission + end # Define any customized permissions here. def custom_permissions @@ -62,4 +66,10 @@ def custom_permissions def curation_concerns Hyrax.config.curation_concerns end + + def download_file_permission + can :read, ActiveFedora::File do |file| + file.mime_type == 'image/tiff' ? false : true + end + end end diff --git a/app/models/concerns/extended_contained_files.rb b/app/models/concerns/extended_contained_files.rb new file mode 100644 index 0000000..4dfb771 --- /dev/null +++ b/app/models/concerns/extended_contained_files.rb @@ -0,0 +1,13 @@ +# rubocop:disable Metrics/LineLength +module ExtendedContainedFiles + extend ActiveSupport::Concern + ORIGINAL_FILE = 'original_file'.freeze + PRESERVATION_MASTER_FILE = 'preservation_master_file'.freeze + TRANSCRIPT = 'transcript'.freeze + + # Extend HydraWorks to support file use properties. + included do + directly_contains_one :preservation_master_file, through: :files, type: ::RDF::URI('http://pcdm.org/use#PreservationMasterFile'), class_name: 'Hydra::PCDM::File' + directly_contains_one :transcript, through: :files, type: ::RDF::URI('http://pcdm.org/use#Transcript'), class_name: 'Hydra::PCDM::File' + end +end diff --git a/app/models/file_set.rb b/app/models/file_set.rb index 37472e6..479fbbe 100644 --- a/app/models/file_set.rb +++ b/app/models/file_set.rb @@ -3,6 +3,10 @@ class FileSet < ActiveFedora::Base include ::RightsOverrideBehavior include ::Hyrax::FileSetBehavior + include ExtendedContainedFiles + + self.indexer = FileSetIndexer + def visibility=(value) case value when VisibilityService::VISIBILITY_TEXT_VALUE_SUPPRESS_DISCOVERY, diff --git a/app/views/hyrax/file_sets/_show_additional_files.html.erb b/app/views/hyrax/file_sets/_show_additional_files.html.erb new file mode 100644 index 0000000..3e809db --- /dev/null +++ b/app/views/hyrax/file_sets/_show_additional_files.html.erb @@ -0,0 +1,35 @@ +<% files = @presenter.solr_document["files_json_tesim"] %> +<% if files && files.count > 1 && can?(:create, ObjectResource) %> +

Additional Files

+
+ + + + + + + + + + + + <% @presenter.solr_document["files_json_tesim"].each do |json_value| + file = JSON.parse(json_value).with_indifferent_access + %> + <% if file[:file_use] != ExtendedContainedFiles::ORIGINAL_FILE %> + + + + + + + + <% end %> + <% end %> + +
<%= t('.file_use') %><%= t('.file_name') %><%= t('.file_size') %><%= t('.date_uploaded') %><%= t('.actions') %>
<%= "pcdm/use#:#{file[:file_use].camelize}" %><%= file[:file_name] %><%= file[:file_size] %><%= file[:date_uploaded].split('T').first if file[:date_uploaded] %> + <%= link_to 'Download', "#{hyrax.download_path(@presenter)}&file=#{file[:file_use]}", + title: "Download #{file[:file_name]}", target: "_blank" %> +
+
+<% end %> diff --git a/app/views/hyrax/file_sets/show.html.erb b/app/views/hyrax/file_sets/show.html.erb new file mode 100644 index 0000000..9dfd576 --- /dev/null +++ b/app/views/hyrax/file_sets/show.html.erb @@ -0,0 +1,19 @@ +
+
+
+ <%= media_display @presenter %> + <%= render 'show_actions', presenter: @presenter %> + <%= render 'single_use_links', presenter: @presenter if @presenter.editor? %> +
+
+
+ <%= render 'file_set_title', presenter: @presenter %> +
+ + <%# TODO: render 'show_descriptions' See https://github.com/projecthydra/hyrax/issues/1481 %> + <%= render 'show_details' %> + <%= render 'show_additional_files' %> + <%= render 'hyrax/users/activity_log', events: @presenter.events %> +
+
+
diff --git a/lib/import/tabular_importer.rb b/lib/import/tabular_importer.rb index 19eaf71..901f022 100644 --- a/lib/import/tabular_importer.rb +++ b/lib/import/tabular_importer.rb @@ -12,6 +12,7 @@ class TabularImporter OBJECT_UNIQUE_ID = 'object unique id'.freeze LEVEL = 'level'.freeze + DEFAULT_FILE_USE = ExtendedContainedFiles::ORIGINAL_FILE attr_reader :user attr_reader :tabular_parser @@ -49,66 +50,31 @@ def import @status = validate return self unless @status - components = [] model = model_to_create(form_attributes) - + components = [] @data.each do |attrs| - attrs = model_attrs(attrs).with_indifferent_access - - # object unique id - attrs.delete :object_unique_id - # level for building complex object: Object, Component, Sub-component - level = attrs['level'] - - # FIXME: file use properties - attrs.delete :file_1_use - attrs.delete :file_2_use - - # FIXME: license, it's changed in upstream in the master branch with Hyrax 1.0 - license = attrs.delete(:license).first if attrs.key? :license - if attrs.key? :rights_override - visibility = VisibilityService.visibility_value(attrs.delete(:rights_override).first) - end - if attrs.key? :visibility_during_embargo - visibility_during_embargo = VisibilityService.visibility_value(attrs.delete(:visibility_during_embargo).first) - end - embargo_release_date = attrs.delete(:embargo_release_date).first if attrs.key? :embargo_release_date - - # source file - process_source_file attrs - - attrs = attrs.merge @form_attributes - - # override license, visibility, embargo_release_date etc. if it is provided in the source metadata - attrs[:license] = license if license.present? - attrs[:visibility] = visibility if visibility.present? - attrs[:visibility_during_embargo] = visibility_during_embargo if visibility_during_embargo.present? - attrs[:embargo_release_date] = embargo_release_date if embargo_release_date.present? - - if level.delete('-').casecmp('OBJECT').zero? && !components.empty? - # ingest object - child_log = Hyrax::Operation.create!(user: @user, - operation_type: "Create Work", - parent: @log) - IngestWorkJob.perform_later(@user, model.to_s, components, child_log) + if attrs['level'].first.delete('-').casecmp('OBJECT').zero? && !components.empty? + ingest_object(@user, model.to_s, components, @log) components = [] - else - components << attrs end + components << process_attributes(attrs) end - if components.count.positive? - # ingest object - child_log = Hyrax::Operation.create!(user: @user, - operation_type: "Create Work", - parent: @log) - IngestWorkJob.perform_later(@user, model.to_s, components, child_log) - end + # Ingest the last object in the batch + ingest_object(@user, model.to_s, components, @log) if components.count.positive? @status = true self end + # Identify the level for building complex object: Object, Component, Sub-component to perform object ingest + def ingest_object(user, model, components, log) + child_log = Hyrax::Operation.create!(user: user, + operation_type: "Create Work", + parent: log) + IngestWorkJob.perform_later(user, model, components, child_log) + end + # validate the source metadata # @return [bool] def validate @@ -365,5 +331,82 @@ def get_match_value(key, val) index = @template.control_values[key].index(val) index.nil? ? val : @template.control_values["#{key} MATCH"][index] end + + def process_attributes(attrs) + attrs = model_attrs(attrs).with_indifferent_access + + # object unique id + attrs.delete :object_unique_id + + # file use properties + add_file_use(attrs, attrs.key?(:file_1_use) ? attrs.delete(:file_1_use) : []) if attrs.key? :file_1_name + add_file_use(attrs, attrs.delete(:file_2_use)) if attrs.key?(:file_2_use) + + # source file + process_source_file attrs + + attrs_dup = attrs.dup # copy for attributes comparing + attrs = attrs.merge @form_attributes + + # Override attributes submitted from the form + property_override(attrs, attrs_dup, :license) + visibility_override(attrs, attrs_dup, :visibility) + visibility_override(attrs, attrs_dup, :visibility_during_embargo) + property_override(attrs, attrs_dup, :embargo_release_date) + + attrs + end + + # override the value of a property + # @param [Hash] attrs + # @param [Hash] attrs_dup: prefer values + # @param [String] attr_name + # @return + def property_override(attrs, attrs_dup, attr_name) + return unless attrs_dup.key? attr_name + value = attrs_dup.delete(attr_name) + attrs[attr_name] = value if value.present? + end + + # override the value of a property + # @param [Hash] attrs + # @param [Hash] attrs_dup: prefer values + # @param [String] attr_name + # @return + def visibility_override(attrs, attrs_dup, attr_name) + return unless attrs_dup.key? attr_name + value = VisibilityService.visibility_value(attrs_dup.delete(attr_name).first) + attrs[attr_name] = value if value.present? + end + + # add file use to object + # @param [Hash] attrs + # @param [String] file_use + # @return + def add_file_use(attrs, file_use) + if file_use.present? + file_use = convert_file_use(file_use.first) + elsif attrs[:file_1_name].present? + file_use = DEFAULT_FILE_USE + end + + attrs[:file_use] ||= [] + attrs[:file_use] << file_use + end + + # convert the value of file use + # @param [String] value + # @return + def convert_file_use(value) + preservation_master_file = ExtendedContainedFiles::PRESERVATION_MASTER_FILE.camelize + transcript = ExtendedContainedFiles::TRANSCRIPT.camelize + original_file = ExtendedContainedFiles::ORIGINAL_FILE.camelize + case value + when preservation_master_file, transcript, original_file + value.underscore + else + value + end + end end end diff --git a/spec/actors/hyrax/actors/create_with_files_actor_spec.rb b/spec/actors/hyrax/actors/create_with_files_actor_spec.rb new file mode 100644 index 0000000..e600065 --- /dev/null +++ b/spec/actors/hyrax/actors/create_with_files_actor_spec.rb @@ -0,0 +1,64 @@ +RSpec.describe Hyrax::Actors::CreateWithFilesActor do + let(:user) { create(:user) } + let(:ability) { ::Ability.new(user) } + let(:work) { create(:object_resource, user: user) } + let(:env) { Hyrax::Actors::Environment.new(work, ability, attributes) } + let(:terminator) { Hyrax::Actors::Terminator.new } + let(:uploaded_file1) { create(:uploaded_file, user: user) } + let(:uploaded_file2) { create(:uploaded_file, user: user) } + let(:uploaded_file_ids) { [uploaded_file1.id, uploaded_file2.id] } + let(:attributes) { { uploaded_files: uploaded_file_ids } } + + subject(:middleware) do + stack = ActionDispatch::MiddlewareStack.new.tap do |middleware| + middleware.use described_class + end + stack.build(terminator) + end + + [:create, :update].each do |mode| + context "on #{mode}" do + before do + allow(terminator).to receive(mode).and_return(true) + end + context "when uploaded_file_ids include nil" do + let(:uploaded_file_ids) { [nil, uploaded_file1.id, nil] } + + it "will discard those nil values when attempting to find the associated UploadedFile" do + expect(AttachFilesToWorkJob).to receive(:perform_later) + expect(Hyrax::UploadedFile).to receive(:find).with([uploaded_file1.id]).and_return([uploaded_file1]) + middleware.public_send(mode, env) + end + end + + context "when uploaded_file_ids belong to me" do + it "attaches files" do + expect(AttachFilesToWorkJob).to receive(:perform_later).with(ObjectResource, + [uploaded_file1, uploaded_file2], [], {}) + expect(middleware.public_send(mode, env)).to be true + end + end + + context "when uploaded_file_ids don't belong to me" do + let(:uploaded_file2) { create(:uploaded_file) } + + it "doesn't attach files" do + expect(AttachFilesToWorkJob).not_to receive(:perform_later) + expect(middleware.public_send(mode, env)).to be false + end + end + + context "when uploaded files with file use" do + let(:file_uses) { ['OriginalFile', 'PreservationMasterFile'] } + let(:attributes) { { uploaded_files: uploaded_file_ids, file_use: file_uses } } + + it "attaches files" do + expect(AttachFilesToFileSetJob).to receive(:perform_later).with(ObjectResource, + [uploaded_file1, uploaded_file2], + file_uses, {}) + expect(middleware.public_send(mode, env)).to be true + end + end + end + end +end diff --git a/spec/actors/hyrax/actors/create_with_remote_files_actor_spec.rb b/spec/actors/hyrax/actors/create_with_remote_files_actor_spec.rb new file mode 100644 index 0000000..b4b8b37 --- /dev/null +++ b/spec/actors/hyrax/actors/create_with_remote_files_actor_spec.rb @@ -0,0 +1,90 @@ +RSpec.describe Hyrax::Actors::CreateWithRemoteFilesActor do + let(:terminator) { Hyrax::Actors::Terminator.new } + let(:actor) { stack.build(terminator) } + let(:stack) do + ActionDispatch::MiddlewareStack.new.tap do |middleware| + middleware.use described_class + end + end + let(:user) { create(:user) } + let(:ability) { Ability.new(user) } + let(:work) { create(:object_resource, user: user) } + let(:url1) { "https://dl.dropbox.com/fake/blah-blah.filepicker-demo.txt.txt" } + let(:url2) { "https://dl.dropbox.com/fake/blah-blah.Getting%20Started.pdf" } + let(:file) { "file:///local/file/here.txt" } + + let(:remote_files) do + [{ url: url1, + expires: "2014-03-31T20:37:36.214Z", + file_name: "filepicker-demo.txt.txt" }, + { url: url2, + expires: "2014-03-31T20:37:36.731Z", + file_name: "Getting+Started.pdf" }] + end + let(:file_uses) { ['original_file'] } + let(:attributes) { { remote_files: remote_files, file_use: file_uses } } + let(:environment) { Hyrax::Actors::Environment.new(work, ability, attributes) } + + before do + allow(terminator).to receive(:create).and_return(true) + end + + context "with source uris that are remote" do + let(:remote_files) do + [{ url: url1, + expires: "2014-03-31T20:37:36.214Z", + file_name: "filepicker-demo.txt.txt" }, + { url: url2, + expires: "2014-03-31T20:37:36.731Z", + file_name: "Getting+Started.pdf" }] + end + let(:file_uses) { ["original_file", "preservation_master_file"] } + + it "attaches files" do + expect(ImportUrlWithFileUseJob).to receive(:perform_later).with(FileSet, + Hyrax::Operation, + URI, + "original_file") + expect(ImportUrlWithFileUseJob).to receive(:perform_later).with(FileSet, + Hyrax::Operation, + URI, + "preservation_master_file") + expect(actor.create(environment)).to be true + end + end + + context "with source uris that are local files" do + let(:remote_files) do + [{ url: file, + expires: "2014-03-31T20:37:36.214Z", + file_name: "here.txt" }] + end + + it "attaches files" do + expect(IngestLocalFileJob).to receive(:perform_later).with(FileSet, "/local/file/here.txt", + user, "original_file") + expect(actor.create(environment)).to be true + end + + context "with spaces" do + let(:file) { "file:///local/file/ pigs .txt" } + + it "attaches files" do + expect(IngestLocalFileJob).to receive(:perform_later).with(FileSet, "/local/file/ pigs .txt", + user, "original_file") + expect(actor.create(environment)).to be true + end + end + + context "attaches files with file use property" do + let(:file_uses) { ['original_file'] } + let(:attributes) { { remote_files: remote_files, file_use: file_uses } } + + it "attaches files" do + expect(IngestLocalFileJob).to receive(:perform_later).with(FileSet, "/local/file/here.txt", + user, file_uses.first) + expect(actor.create(environment)).to be true + end + end + end +end diff --git a/spec/controllers/hyrax/downloads_controller_spec.rb b/spec/controllers/hyrax/downloads_controller_spec.rb index 46f2a16..1ebeff3 100644 --- a/spec/controllers/hyrax/downloads_controller_spec.rb +++ b/spec/controllers/hyrax/downloads_controller_spec.rb @@ -10,6 +10,10 @@ end let(:default_image) { ActionController::Base.helpers.image_path 'default.png' } let(:world_image) { fixture_path + '/files/world.png' } + let(:tiff_image) { fixture_path + '/files/file_3.tif' } + let(:tiff_file_set) do + FactoryGirl.create(:file_with_work, user: user, content: File.open(tiff_image)) + end context "when user doesn't have access" do let(:another_user) { FactoryGirl.create(:user) } @@ -68,6 +72,49 @@ end end + context "when the original_file is a tiff" do + context "public user that don't have access" do + it 'sends the lower resolution image' do + get :show, params: { id: tiff_file_set } + expect(response.body).not_to eq tiff_file_set.original_file.content + end + end + + context "admin user that has access" do + let(:admin_user) { FactoryGirl.create(:admin) } + + before { sign_in admin_user } + + it 'sends the original file' do + get :show, params: { id: tiff_file_set } + expect(response.body).to eq tiff_file_set.original_file.content + end + end + end + + context "when preservation_master_file" do + before do + Hydra::Works::AddFileToFileSet.call(tiff_file_set, File.open(world_image), :preservation_master_file) + tiff_file_set.save! + end + + it "redirects to the default image" do + get :show, params: { id: tiff_file_set.to_param, file: :preservation_master_file } + expect(response).to redirect_to default_image + end + + context "admin user that has access" do + let(:admin_user) { FactoryGirl.create(:admin) } + + before { sign_in admin_user } + + it "sends the preservation_master_file" do + get :show, params: { id: tiff_file_set, file: :preservation_master_file } + expect(response.body).to eq tiff_file_set.preservation_master_file.content + end + end + end + context 'metadata-only and culturally-sensitive object resources' do let(:admin_user) { FactoryGirl.create(:admin) } let!(:metadata_only_object_resource) do diff --git a/spec/factories/uploaded_files.rb b/spec/factories/uploaded_files.rb new file mode 100644 index 0000000..77ce31c --- /dev/null +++ b/spec/factories/uploaded_files.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :uploaded_file, class: Hyrax::UploadedFile do + user + end +end diff --git a/spec/fixtures/files/file_3.tif b/spec/fixtures/files/file_3.tif new file mode 100644 index 0000000..7c243b0 Binary files /dev/null and b/spec/fixtures/files/file_3.tif differ diff --git a/spec/fixtures/imports/csv_import_test.csv b/spec/fixtures/imports/csv_import_test.csv index 9b7dd9d..c9df3d7 100644 --- a/spec/fixtures/imports/csv_import_test.csv +++ b/spec/fixtures/imports/csv_import_test.csv @@ -1 +1 @@ -object unique id,level,title,file path,file 1 name,file 1 use,file 2 name,file 2 name,subject:topic,agent:creator,agent:contributor,subject:spatial,date:created,note:note,identifier:doi,language,type of resource,copyright jurisdiction,copyright status,rights holder,license,related resource object#1,Object,Test Object One,,,,,,SUBJECT:TOPIC,AGENT:CREATOR,AGENT:CONTRIBUTOR,SUBJECT:SPATIAL,"Decenber 10, 2016 @{begin=2016-12-10 ; end=2016-12-10 }",NOTE:NOTE,IDENTIFIER:DOI,eng - English|zxx - No linguistic content; Not applicable ,mixed material | still image,US - United States of America,copyrighted,AGENT:RIGHTS_HOLDER,MIT,RELATED RESOURCE @{relatedType=relation; url=http://test.com/related_resource/relation} object#1,Component,Test Component One,,file_1.jpg,image-source,,,,,,,,,,,,,,,, object#1,Sub-component,Test Sub-component One,,file_2.jpg,image-source,,,,,,,,,,,,,,,, \ No newline at end of file +object unique id,level,title,file path,file 1 name,file 1 use,file 2 name,file 2 name,subject:topic,agent:creator,agent:contributor,subject:spatial,date:created,note:note,identifier:doi,language,type of resource,copyright jurisdiction,copyright status,rights holder,license,related resource object#1,Object,Test Object One,,,,,,SUBJECT:TOPIC,AGENT:CREATOR,AGENT:CONTRIBUTOR,SUBJECT:SPATIAL,"Decenber 10, 2016 @{begin=2016-12-10 ; end=2016-12-10 }",NOTE:NOTE,IDENTIFIER:DOI,eng - English|zxx - No linguistic content; Not applicable ,mixed material | still image,US - United States of America,copyrighted,AGENT:RIGHTS_HOLDER,MIT,RELATED RESOURCE @{relatedType=relation; url=http://test.com/related_resource/relation} object#1,Component,Test Component One,,file_1.jpg,,,,,,,,,,,,,,,,, object#1,Sub-component,Test Sub-component One,,file_2.jpg,OriginalFile,image.jpg,PreservationMasterFile,,,,,,,,,,,,,, \ No newline at end of file diff --git a/spec/fixtures/imports/excel_xl_import_test.xlsx b/spec/fixtures/imports/excel_xl_import_test.xlsx index d2a74f9..4e79aaf 100644 Binary files a/spec/fixtures/imports/excel_xl_import_test.xlsx and b/spec/fixtures/imports/excel_xl_import_test.xlsx differ diff --git a/spec/hyrax/file_sets/show.html.erb_spec.rb b/spec/hyrax/file_sets/show.html.erb_spec.rb new file mode 100644 index 0000000..c4c70de --- /dev/null +++ b/spec/hyrax/file_sets/show.html.erb_spec.rb @@ -0,0 +1,66 @@ +RSpec.describe 'hyrax/file_sets/show.html.erb', type: :view do + let(:user) { double(user_key: 'sarah', twitter_handle: 'test') } + let(:file_set) do + build(:file_set, id: '123', depositor: user.user_key, title: ['My Title'], user: user, visibility: 'open') + end + let(:ability) { double } + let(:solr_doc) { SolrDocument.new(file_set.to_solr) } + let(:presenter) { Hyrax::FileSetPresenter.new(solr_doc, ability) } + let(:mock_metadata) do + { + format: ["Tape"], + long_term: ["x" * 255], + multi_term: ["1", "2", "3", "4", "5", "6", "7", "8"], + string_term: 'oops, I used a string instead of an array', + logged_fixity_status: "Fixity checks have not yet been run on this file" + }.to_h + end + + before do + view.lookup_context.prefixes.push 'hyrax/base' + allow(view).to receive(:can?).with(:edit, SolrDocument).and_return(false) + allow(ability).to receive(:can?).with(:edit, SolrDocument).and_return(false) + allow(presenter).to receive(:fixity_status).and_return(mock_metadata) + assign(:presenter, presenter) + assign(:document, solr_doc) + assign(:fixity_status, "none") + allow(FileSet).to receive(:find).with('123').and_return(file_set) + end + + describe 'title heading' do + before do + stub_template 'shared/_title_bar.html.erb' => 'Title Bar' + stub_template 'shared/_citations.html.erb' => 'Citation' + render + end + it 'shows the title' do + expect(rendered).to have_selector 'h1', text: 'My Title' + end + end + + it "does not render single-use links" do + expect(rendered).not_to have_selector('table.single-use-links') + end + + describe 'preservation_master_file' do + let(:files_json) do + ["{\"file_use\":\"preservation_master_file\", \"file_name\":\"image.jpg\"}", + "{\"file_use\":\"original_file\"}"] + end + let(:file_set_document) { SolrDocument.new(id: "123", has_model_ssim: ["FileSet"], files_json_tesim: files_json) } + let(:file_set_presenter) { FileSetPresenter.new(file_set_document, ability) } + + before do + allow(view).to receive(:can?).with(:create, ObjectResource).and_return(true) + allow(presenter).to receive(:fixity_status).and_return(mock_metadata) + assign(:presenter, file_set_presenter) + assign(:document, file_set_document) + render + end + + it 'show the download link' do + expect(rendered).to have_content('image.jpg') + expect(rendered).to have_link('Download', href: "#{hyrax.download_path(presenter)}&file=preservation_master_file") + end + end +end diff --git a/spec/import/tabular_importer_spec.rb b/spec/import/tabular_importer_spec.rb index d2910f8..ca42236 100644 --- a/spec/import/tabular_importer_spec.rb +++ b/spec/import/tabular_importer_spec.rb @@ -22,8 +22,16 @@ let(:upload1) { Hyrax::UploadedFile.create(user: user, file: file1) } let(:file2) { File.open(fixture_path + '/files/file_2.jpg') } let(:upload2) { Hyrax::UploadedFile.create(user: user, file: file2) } + let(:file3) { File.open(fixture_path + '/files/image.jpg') } + let(:upload3) { Hyrax::UploadedFile.create(user: user, file: file3) } let(:metadata) { { model: 'ObjectResource' } } - let(:uploaded_files) { { upload1.file.filename => upload1.id.to_s, upload2.file.filename => upload2.id.to_s } } + let(:uploaded_files) do + { + upload1.file.filename => upload1.id.to_s, + upload2.file.filename => upload2.id.to_s, + upload3.file.filename => upload3.id.to_s + } + end let(:remote_files) { {} } let(:errors) { double(full_messages: "It's broke!") } let(:work) { double(errors: errors) } @@ -51,7 +59,8 @@ expect(env.attributes).to include(title: ['Test Component One'], uploaded_files: [upload1.id.to_s]) end.and_return(true) expect(actor).to receive(:create).with(Hyrax::Actors::Environment) do |env| - expect(env.attributes).to include(title: ['Test Sub-component One'], uploaded_files: [upload2.id.to_s]) + expect(env.attributes).to include(title: ['Test Sub-component One'], + uploaded_files: [upload2.id.to_s, upload3.id.to_s]) end.and_return(true) expect(subject.status).to eq true end @@ -112,7 +121,8 @@ expect(env.attributes).to include(title: ['Test Component One'], uploaded_files: [upload1.id.to_s]) end.and_return(true) expect(actor).to receive(:create).with(Hyrax::Actors::Environment) do |env| - expect(env.attributes).to include(title: ['Test Sub-component One'], uploaded_files: [upload2.id.to_s]) + expect(env.attributes).to include(title: ['Test Sub-component One'], + uploaded_files: [upload2.id.to_s, upload3.id.to_s]) end.and_return(true) expect(Hyrax.config.callback).to receive(:run).with(:after_batch_create_success, user) expect(subject.status).to eq true diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 4a13b30..3adc3d3 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -30,6 +30,22 @@ FactoryGirl.build(:private_collection, user: admin_user) end + let(:jpeg_file_with_work) do + FactoryGirl.create(:file_with_work, user: admin_user, content: File.open(fixture_path + '/files/image.jpg')) + end + + let(:tiff_file_with_work) do + FactoryGirl.create(:file_with_work, user: admin_user, content: File.open(fixture_path + '/files/file_3.tif')) + end + + let(:preservation_master_file_with_work) do + FactoryGirl.create(:file_with_work, user: admin_user) + end + + let(:preservation_master_file) do + File.open(fixture_path + '/files/file_3.tif') + end + let(:admin_user) { FactoryGirl.create(:admin) } let(:editor) { FactoryGirl.create(:editor) } let(:curator) { FactoryGirl.create(:curator) } @@ -50,6 +66,10 @@ allow(subject.cache).to receive(:get).with(obj.id) .and_return(Hydra::PermissionsSolrDocument.new(obj.to_solr, nil)) end + + Hydra::Works::AddFileToFileSet.call(preservation_master_file_with_work, + preservation_master_file, :preservation_master_file) + preservation_master_file_with_work.save! end describe 'as an admin' do @@ -88,6 +108,10 @@ is_expected.to be_able_to(:create, Role.new) is_expected.to be_able_to(:destroy, Role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end @@ -124,6 +148,10 @@ is_expected.not_to be_able_to(:create, Role.new) is_expected.not_to be_able_to(:destroy, role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end @@ -157,6 +185,10 @@ is_expected.not_to be_able_to(:create, Role.new) is_expected.not_to be_able_to(:destroy, role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end @@ -197,6 +229,10 @@ is_expected.not_to be_able_to(:create, Role.new) is_expected.not_to be_able_to(:destroy, role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.not_to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.not_to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end @@ -230,6 +266,10 @@ is_expected.not_to be_able_to(:create, Role.new) is_expected.not_to be_able_to(:destroy, role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.not_to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.not_to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end @@ -263,6 +303,10 @@ is_expected.not_to be_able_to(:create, Role.new) is_expected.not_to be_able_to(:destroy, role) + + is_expected.to be_able_to(:read, jpeg_file_with_work.original_file) + is_expected.not_to be_able_to(:read, tiff_file_with_work.original_file) + is_expected.not_to be_able_to(:read, preservation_master_file_with_work.preservation_master_file) end end end