Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/actors/file_set_attach_files_actor.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What change was needed here that required us overriding the (presumably) upstream FileSetAttachFilesActor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we just extend/override the Hyrax::Actors::FileSetActor to accept a relation/file use parameter.

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
62 changes: 62 additions & 0 deletions app/actors/hyrax/actors/create_with_files_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Hyrax
module Actors
# Creates a work and attaches files to the work
class CreateWithFilesActor < Hyrax::Actors::AbstractActor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What change was needed here that required us overriding the (presumably) upstream CreateWithFilesActor ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to override it to extract file uses attributes from the object metadata and extend method attach_files(files, env, file_uses) to accept the file uses parameter for ingest.

# @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
104 changes: 104 additions & 0 deletions app/actors/hyrax/actors/create_with_remote_files_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
module Hyrax
module Actors
# Attaches remote files to the work
class CreateWithRemoteFilesActor < Hyrax::Actors::AbstractActor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What change was needed here that required us overriding the (presumably) upstream CreateWithRemoteFilesActor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as above to extract file uses from object metadata and extend it to accept the file uses properties for ingest.

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
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
35 changes: 33 additions & 2 deletions app/controllers/hyrax/downloads_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
50 changes: 50 additions & 0 deletions app/indexers/file_set_indexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,54 @@

class FileSetIndexer < Hyrax::FileSetIndexer
self.thumbnail_path_service = ::ThumbnailPathService

def generate_solr_document
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was there previously no Solr document content generated for FileSets? Or are we overriding what was there and generating our own here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think hyrax does have solr document generate for FileSets but unfortunately it won't support ingest/store multiples files in one fileset at this time. And there are no strategic to store technical metadata for other files other than the original file in the same fileset. So we need to extend it to include technical metadata for all other files in Solr document so that we can expose them for UI displaying.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that makes sense. Thanks for clarifying 👍

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
75 changes: 75 additions & 0 deletions app/jobs/attach_files_to_file_set_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Converts UploadedFiles into FileSets and attaches them to works.
class AttachFilesToFileSetJob < AttachFilesToWorkJob
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What change was needed here that required us overriding the (presumably) upstream AttachFilesToFileSetJob?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to extend the functionality to accept the file use parameter and perform ingest files with those files use properties provided as well.

queue_as Hyrax.config.ingest_queue_name

ORIGINAL_FILE = ExtendedContainedFiles::ORIGINAL_FILE

# @param [ActiveFedora::Base] work - the work object
# @param [Array<Hyrax::UploadedFile>] 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
Loading