Skip to content

Commit

Permalink
Refactor FileNode behavior.
Browse files Browse the repository at this point in the history
Rather than force the parent application to create metadata in the
metadata store themselves, Files now accept a `metadata_resource.` Some
StorageAdapters can store this data natively, others require a metadata
store of their own to work.
  • Loading branch information
tpendragon committed Jul 27, 2017
1 parent 9137600 commit ed409f5
Show file tree
Hide file tree
Showing 30 changed files with 333 additions and 230 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Expand Up @@ -20,6 +20,7 @@ PATH
dry-types
hydra-access-controls
hydra-derivatives
hydra-works
json-ld
pg
rdf
Expand Down
1 change: 1 addition & 0 deletions app/change_sets/page_change_set.rb
Expand Up @@ -4,6 +4,7 @@ class PageChangeSet < Valkyrie::ChangeSet
self.fields = Page.fields - [:id, :internal_resource, :created_at, :updated_at]
property :viewing_hint, multiple: false
property :title, required: true
property :files, virtual: true, multiple: true

private

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/downloads_controller.rb
Expand Up @@ -16,7 +16,7 @@ class FileWithMetadata < Dry::Struct

def load_file
binary_file = storage_adapter.find_by(id: file_identifier)
FileWithMetadata.new(id: file_identifier, file: binary_file, mime_type: asset.mime_type, original_name: asset.original_filename.first)
FileWithMetadata.new(id: file_identifier, file: binary_file, mime_type: binary_file.metadata_resource.mime_type, original_name: binary_file.metadata_resource.original_filename)
end

# Customize the :download ability in your Ability class, or override this method
Expand Down
2 changes: 1 addition & 1 deletion app/decorators/file_set_decorator.rb
Expand Up @@ -3,6 +3,6 @@ class FileSetDecorator < ApplicationDecorator
delegate_all

def download_id
member_ids.first
id
end
end
21 changes: 11 additions & 10 deletions app/derivative_services/image_derivative_service.rb
Expand Up @@ -18,12 +18,14 @@ def new(change_set)
::ImageDerivativeService.new(change_set: change_set, original_file: original_file(change_set), change_set_persister: change_set_persister, image_config: image_config, use: use)
end

def original_file(resource)
members(resource).find { |x| x.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile) }
def original_file(model)
files(model).find { |x| x.metadata_resource.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile) }
end

def members(resource)
metadata_adapter.query_service.find_members(resource: resource)
def files(model)
model.file_identifiers.map do |id|
Valkyrie::StorageAdapter.find_by(id: id)
end
end

class ImageConfig < Dry::Struct
Expand All @@ -37,7 +39,6 @@ class ImageConfig < Dry::Struct
attr_reader :change_set, :original_file, :image_config, :use, :change_set_persister
delegate :metadata_adapter, :storage_adapter, to: :change_set_persister
delegate :width, :height, :format, :output_name, to: :image_config
delegate :mime_type, to: :original_file
delegate :persister, to: :metadata_adapter
def initialize(change_set:, original_file:, change_set_persister:, image_config:, use:)
@change_set = change_set
Expand All @@ -51,6 +52,10 @@ def image_mime_type
image_config.mime_type
end

def mime_type
original_file.metadata_resource.mime_type
end

def create_derivatives
Hydra::Derivatives::ImageDerivatives.create(filename,
outputs: [{ label: :thumbnail, format: format, size: "#{width}x#{height}>", url: URI("file://#{temporary_output.path}") }])
Expand All @@ -75,11 +80,7 @@ def build_file
def cleanup_derivatives; end

def filename
return Pathname.new(file_object.io.path) if file_object.io.respond_to?(:path) && File.exist?(file_object.io.path)
end

def file_object
@file_object ||= Valkyrie::StorageAdapter.find_by(id: original_file.file_identifiers[0])
return Pathname.new(original_file.io.path) if original_file.io.respond_to?(:path) && File.exist?(original_file.io.path)
end

def temporary_output
Expand Down
Expand Up @@ -4,27 +4,31 @@
# defines the Apache Tika based characterization service a ValkyrieFileCharacterization service
# @since 0.1.0
class TikaFileCharacterizationService
attr_reader :file_node, :persister
def initialize(file_node:, persister:)
@file_node = file_node
@persister = persister
attr_reader :file, :storage_adapter
def initialize(file:, storage_adapter:)
@file = file
@storage_adapter = storage_adapter
end

# characterizes the file_node passed into this service
# Default options are:
# save: true
# @param save [Boolean] should the persister save the file_node after Characterization
# characterizes the file passed into this service
# @return [FileNode]
# @example characterize a file and persist the changes by default
# Valkyrie::FileCharacterizationService.for(file_node, persister).characterize
# @example characterize a file and do not persist the changes
# Valkyrie::FileCharacterizationService.for(file_node, persister).characterize(save: false)
def characterize(save: true)
# @example characterize a file and persist the changes
# Valkyrie::FileCharacterizationService.for(file, storage_adapter).characterize
def characterize
result = JSON.parse(json_output).last
@file_characterization_attributes = FileCharacterizationAttributes.new(width: result['tiff:ImageWidth'], height: result['tiff:ImageLength'], mime_type: result['Content-Type'], checksum: checksum)
@file_node = @file_node.new(@file_characterization_attributes.to_h)
@persister.save(resource: @file_node) if save
@file_node
file_characterization_attributes = FileCharacterizationAttributes.new(old_resource.to_h.except(:internal_resource).merge(
width: result['tiff:ImageWidth'],
height: result['tiff:ImageLength'],
mime_type: result['Content-Type'],
checksum: checksum
))
storage_adapter.update_metadata(file: file, metadata_resource: file_characterization_attributes)
file.metadata_resource = file_characterization_attributes
file
end

def old_resource
file.metadata_resource || FileCharacterizationAttributes.new
end

# Provides the SHA256 hexdigest string for the file
Expand All @@ -37,27 +41,24 @@ def json_output
"[#{RubyTikaApp.new(filename.to_s).to_json.gsub('}{', '},{')}]"
end

# Determines the location of the file on disk for the file_node
# Determines the location of the file on disk for the file
# @return Pathname
def filename
return Pathname.new(file_object.io.path) if file_object.io.respond_to?(:path) && File.exist?(file_object.io.path)
end

# Provides the file attached to the file_node
# @return Valkyrie::FileRepository::File
def file_object
@file_object ||= Valkyrie::StorageAdapter.find_by(id: @file_node.file_identifiers[0])
return Pathname.new(file.io.path) if file.io.respond_to?(:path) && File.exist?(file.io.path)
end

def valid?
true
end

# Class for updating characterization attributes on the FileNode
class FileCharacterizationAttributes < Dry::Struct
attribute :width, Valkyrie::Types::Int
attribute :height, Valkyrie::Types::Int
attribute :mime_type, Valkyrie::Types::String
attribute :checksum, Valkyrie::Types::String
class FileCharacterizationAttributes < Valkyrie::Resource
attribute :width, Valkyrie::Types::Set.member(Valkyrie::Types::Coercible::Int)
attribute :height, Valkyrie::Types::Set.member(Valkyrie::Types::Coercible::Int)
attribute :mime_type, Valkyrie::Types::Set
attribute :label, Valkyrie::Types::Set
attribute :original_filename, Valkyrie::Types::Set
attribute :use, Valkyrie::Types::Set
attribute :checksum, Valkyrie::Types::Set
end
end
1 change: 1 addition & 0 deletions app/models/page.rb
Expand Up @@ -4,4 +4,5 @@ class Page < Valkyrie::Resource
attribute :id, Valkyrie::Types::ID.optional
attribute :title, Valkyrie::Types::Set
attribute :viewing_hint, Valkyrie::Types::Set
attribute :member_ids, Valkyrie::Types::Array
end
65 changes: 42 additions & 23 deletions app/services/file_appender.rb
@@ -1,5 +1,11 @@
# frozen_string_literal: true
class FileAppender
class MetadataResource < Valkyrie::Resource
attribute :label, Valkyrie::Types::Set
attribute :original_filename, Valkyrie::Types::Set
attribute :mime_type, Valkyrie::Types::Set
attribute :use, Valkyrie::Types::Set
end
attr_reader :storage_adapter, :persister, :files
def initialize(storage_adapter:, persister:, files:)
@storage_adapter = storage_adapter
Expand All @@ -9,42 +15,55 @@ def initialize(storage_adapter:, persister:, files:)

def append_to(resource)
return resource if files.blank?
file_sets = build_file_sets || file_nodes
return append_files(resource: resource) if appending_derivative?
file_sets = build_file_sets
resource.member_ids = resource.member_ids + file_sets.map(&:id)
resource
persister.save(resource: resource)
end

def build_file_sets
return if processing_derivatives?
file_nodes.map do |node|
file_set = create_file_set(node)
Valkyrie::DerivativeService.for(FileSetChangeSet.new(file_set)).create_derivatives if node.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile)
files.map do |file|
file_set = create_file_set(file)
file_set = append_file(file: file, resource: file_set)
Valkyrie::DerivativeService.for(FileSetChangeSet.new(file_set)).create_derivatives
file_set
end
end

def processing_derivatives?
!file_nodes.first.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile)
def metadata(file:)
MetadataResource.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 characterization_data(file:)
Valkyrie::FileCharacterizationService.for(file: file, storage_adapter: storage_adapter).characterize
end

def append_files(resource:)
files.map do |file|
append_file(file: file, resource: resource)
end
end

def append_file(file:, resource:)
uploaded_file = uploaded_file(file: file, resource: resource)
uploaded_file = characterization_data(file: uploaded_file)
resource.file_identifiers = resource.file_identifiers + [uploaded_file.id]
persister.save(resource: resource)
end

def file_nodes
@file_nodes ||=
begin
files.map do |file|
create_node(file)
end
end
def uploaded_file(file:, resource:)
storage_adapter.upload(file: file, resource: resource, metadata_resource: metadata(file: file))
end

def create_node(file)
node = persister.save(resource: FileNode.for(file: file))
file = storage_adapter.upload(file: file, resource: node)
node.file_identifiers = node.file_identifiers + [file.id]
node = Valkyrie::FileCharacterizationService.for(file_node: node, persister: persister).characterize(save: false)
persister.save(resource: node)
def create_file_set(file)
persister.save(resource: FileSet.new(title: file.original_filename))
end

def create_file_set(file_node)
persister.save(resource: FileSet.new(title: file_node.original_filename, member_ids: file_node.id))
def appending_derivative?
files.find do |file|
metadata(file: file).use.include?(Valkyrie::Vocab::PCDMUse.ServiceFile)
end.present?
end
end
13 changes: 8 additions & 5 deletions config/initializers/valkyrie.rb
Expand Up @@ -27,11 +27,6 @@
:fedora
)

Valkyrie::StorageAdapter.register(
Valkyrie::Storage::Disk.new(base_path: Rails.root.join("tmp", "files")),
:disk
)

Valkyrie::StorageAdapter.register(
Valkyrie::Storage::Memory.new,
:memory
Expand All @@ -45,6 +40,14 @@
:indexing_persister
)

Valkyrie::StorageAdapter.register(
Valkyrie::Storage::Disk.new(
base_path: Rails.root.join("tmp", "files"),
metadata_adapter: Valkyrie::MetadataAdapter.find(:indexing_persister)
),
:disk
)

# ImageDerivativeService needs its own change_set_persister because the
# derivatives may not be in the primary metadata/file storage.
Valkyrie::DerivativeService.services << ImageDerivativeService::Factory.new(
Expand Down
2 changes: 1 addition & 1 deletion spec/change_set_persisters/change_set_persister_spec.rb
Expand Up @@ -31,7 +31,7 @@
change_set.files = [file]

output = change_set_persister.save(change_set: change_set)
members = query_service.find_members(resource: output)
members = query_service.find_members(resource: output).to_a

expect(members.length).to eq 1
expect(members[0]).to be_kind_of FileSet
Expand Down
19 changes: 9 additions & 10 deletions spec/controllers/books_controller_spec.rb
Expand Up @@ -24,19 +24,18 @@
book = query_service.find_by(id: Valkyrie::ID.new(id))
expect(book.member_ids).not_to be_blank
file_set = query_service.find_members(resource: book).first
files = query_service.find_members(resource: file_set)
file = files.find { |x| x.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile) }
files = file_set.file_identifiers.map { |x| Valkyrie::StorageAdapter.find_by(id: x) }
file = files.find { |x| x.metadata_resource.use.include?(Valkyrie::Vocab::PCDMUse.OriginalFile) }

expect(file.file_identifiers).not_to be_empty
expect(file.label).to contain_exactly "example.tif"
expect(file.original_filename).to contain_exactly "example.tif"
expect(file.mime_type).to contain_exactly "image/tiff"
expect(file.use).to contain_exactly Valkyrie::Vocab::PCDMUse.OriginalFile
expect(file.metadata_resource.mime_type).to contain_exactly "image/tiff"
expect(file.metadata_resource.label).to contain_exactly "example.tif"
expect(file.metadata_resource.original_filename).to contain_exactly "example.tif"
expect(file.metadata_resource.use).to contain_exactly Valkyrie::Vocab::PCDMUse.OriginalFile
expect(file.metadata_resource.width).to eq [200]

# Generate derivatives
derivative = files.find { |x| x.use.include?(Valkyrie::Vocab::PCDMUse.ServiceFile) }
derivative = files.find { |x| x.metadata_resource.use.include?(Valkyrie::Vocab::PCDMUse.ServiceFile) }
expect(derivative).to be_present
expect(derivative.use).to include Valkyrie::Vocab::PCDMUse.ThumbnailImage
expect(derivative.metadata_resource.use).to include Valkyrie::Vocab::PCDMUse.ThumbnailImage
end
end

Expand Down
17 changes: 7 additions & 10 deletions spec/controllers/downloads_controller_spec.rb
Expand Up @@ -6,20 +6,17 @@
before do
sign_in user if user
end
let(:persister) { Valkyrie.config.metadata_adapter.persister }
let(:storage_adapter) { Valkyrie.config.storage_adapter }
let(:file) { fixture_file_upload('files/example.tif', 'image/tiff') }
let(:change_set_persister) { ChangeSetPersister.new(metadata_adapter: Valkyrie.config.metadata_adapter, storage_adapter: Valkyrie.config.storage_adapter) }
let(:change_set) { PageChangeSet.new(Page.new, files: [file]) }
let(:resource) { change_set_persister.save(change_set: change_set) }
let(:uploaded_file) { Valkyrie::StorageAdapter.find_by(id: file_set.file_identifiers.first) }
let(:file_set) { Valkyrie.config.metadata_adapter.query_service.find_members(resource: resource).first }

describe "GET /downloads/:id" do
context "when there's a FileNode with that ID" do
let(:uploaded_file) { storage_adapter.upload(file: file, resource: file_node) }
let(:file_node) { persister.save(resource: FileNode.new(mime_type: file.content_type, original_filename: file.original_filename)) }
before do
file_node.file_identifiers = uploaded_file.id
persister.save(resource: file_node)
end
context "when there's a FileSet with that ID" do
it "returns it" do
get :show, params: { id: file_node.id.to_s }
get :show, params: { id: file_set.id.to_s }

uploaded_file.rewind

Expand Down
7 changes: 3 additions & 4 deletions spec/derivative_services/image_derivative_service_spec.rb
Expand Up @@ -45,12 +45,11 @@
derivative_service.new(valid_change_set).create_derivatives

reloaded = query_service.find_by(id: valid_resource.id)
members = query_service.find_members(resource: reloaded)
derivative = members.find { |x| x.use.include?(Valkyrie::Vocab::PCDMUse.ServiceFile) }
members = reloaded.file_identifiers.map { |x| Valkyrie::StorageAdapter.find_by(id: x) }
derivative = members.find { |x| x.metadata_resource.use.include?(Valkyrie::Vocab::PCDMUse.ServiceFile) }

expect(derivative).to be_present
derivative_file = Valkyrie::StorageAdapter.find_by(id: derivative.file_identifiers.first)
image = MiniMagick::Image.open(derivative_file.io.path)
image = MiniMagick::Image.open(derivative.io.path)
expect(image.width).to eq 105
expect(image.height).to eq 150
end
Expand Down

0 comments on commit ed409f5

Please sign in to comment.