Skip to content

Commit

Permalink
Files should be independent of the ActiveFedora::Base object.
Browse files Browse the repository at this point in the history
This enables files to be created and saved without a container.
  • Loading branch information
jcoyne committed Nov 6, 2014
1 parent 2db7e03 commit dccebd0
Show file tree
Hide file tree
Showing 34 changed files with 263 additions and 271 deletions.
1 change: 1 addition & 0 deletions lib/active_fedora.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Rollback < RuntimeError; end # :nodoc:
autoload :FedoraAttributes
autoload :File
autoload :FileConfigurator
autoload :FilePathBuilder
autoload :FilesHash
autoload :Indexing
autoload :LdpResource
Expand Down
18 changes: 9 additions & 9 deletions lib/active_fedora/attached_files.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,21 @@ def load_attached_files
dsid = ds_uri.to_s.sub(uri + '/', '')
reflection = local_ds_specs.delete(dsid)
ds = reflection ? reflection.build_datastream(self) : ActiveFedora::File.new(self, dsid)
attach_file(ds)
attach_file(ds, dsid)
configure_datastream(attached_files[dsid], reflection)
end
local_ds_specs.each do |name, reflection|
ds = reflection.build_datastream(self)
attach_file(ds)
attach_file(ds, name)
configure_datastream(ds, reflection)
end
end

# Adds datastream to the object.
# @return [String] dsid of the added datastream
def attach_file(file, opts={})
attached_files[file.dsid] = file
file.dsid
def attach_file(file, dsid, opts={})
attached_files[dsid] = file
dsid
end

def add_datastream(datastream, opts={})
Expand All @@ -112,17 +112,17 @@ def metadata_streams
# @option opts [String] :original_name The original name of the file (used for Content-Disposition)
def add_file_datastream(file, opts={})
attrs = {blob: file, prefix: opts[:prefix]}
ds = create_datastream(self.class.datastream_class_for_name(opts[:dsid]), opts[:dsid], attrs)
file_path = FilePathBuilder.build(self, opts[:dsid], opts[:prefix])
ds = create_datastream(self.class.datastream_class_for_name(file_path), file_path, attrs)
ds.mime_type = if opts[:mimeType]
Deprecation.warn AttachedFiles, "The :mimeType option to add_file_datastream is deprecated and will be removed in active-fedora 9.0. Use :mime_type instead", caller
opts[:mimeType]
else
opts[:mime_type]
end
ds.original_name = opts[:original_name] if opts.key?(:original_name)
attach_file(ds).tap do |dsid|
self.class.build_datastream_accessor(dsid) unless respond_to? dsid
end
attach_file(ds, file_path)
self.class.build_datastream_accessor(file_path) unless respond_to? file_path
end

def create_datastream(type, dsid, opts={})
Expand Down
19 changes: 9 additions & 10 deletions lib/active_fedora/datastreams/nokogiri_datastreams.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def ng_xml
## Load up the template
xml = self.class.xml_template
else
xml = Nokogiri::XML::Document.parse(datastream_content)
xml = Nokogiri::XML::Document.parse(remote_content)
end
self.class.decorate_ng_xml xml
end
Expand Down Expand Up @@ -50,27 +50,27 @@ def refresh_attributes
@ng_xml = nil
end

# don't want content eagerly loaded by proxy, so implementing methods that would be implemented by define_attribute_methods
# don't want content eagerly loaded by proxy, so implementing methods that would be implemented by define_attribute_methods
def ng_xml_will_change!
changed_attributes['ng_xml'] = nil
end

def ng_xml_doesnt_change!
changed_attributes.delete('ng_xml')
end
# don't want content eagerly loaded by proxy, so implementing methods that would be implemented by define_attribute_methods

# don't want content eagerly loaded by proxy, so implementing methods that would be implemented by define_attribute_methods
def ng_xml_changed?
changed_attributes.has_key? 'ng_xml'
end

def datastream_content
def remote_content
@datastream_content ||= Nokogiri::XML(super).to_xml {|config| config.no_declaration}.strip
end

def content=(new_content)
if datastream_content != new_content
ng_xml_will_change!
if remote_content != new_content
ng_xml_will_change!
@ng_xml = Nokogiri::XML::Document.parse(new_content)
super(@ng_xml.to_s.strip)
end
Expand Down Expand Up @@ -102,7 +102,7 @@ def to_xml(xml = nil)
raise "You can only pass instances of Nokogiri::XML::Node into this method. You passed in #{xml}"
end
end

return xml.to_xml.strip
end

Expand All @@ -118,7 +118,6 @@ def autocreate?
def xml_loaded
instance_variable_defined? :@ng_xml
end

end
end
end
2 changes: 1 addition & 1 deletion lib/active_fedora/delegated_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(field, dsid, datastream_class, args={})
def primary_solr_name
@datastream ||= datastream_class.new(ActiveFedora::Base.new, dsid)
if @datastream.respond_to?(:primary_solr_name)
@datastream.primary_solr_name(field)
@datastream.primary_solr_name(field, dsid)
else
raise NoMethodError, "the datastream '#{datastream_class}' doesn't respond to 'primary_solr_name'"
end
Expand Down
138 changes: 50 additions & 88 deletions lib/active_fedora/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module ActiveFedora

#This class represents a Fedora datastream
class File
include AttributeMethods
include AttributeMethods # allows 'content' to be tracked
include ActiveModel::Dirty
extend ActiveTriples::Properties
generate_method 'content'
Expand All @@ -11,68 +11,65 @@ class File
define_model_callbacks :save, :create, :destroy
define_model_callbacks :initialize, only: :after

attr_reader :digital_object, :dsid, :uri
attr_accessor :last_modified

# @param digital_object [DigitalObject] the digital object that this object belongs to
# @param dsid [String] the datastream id, if this is nil, a datastream id will be generated.
# @param parent_or_url [ActiveFedora::Base, String, NilClass] the parent resource or the URI of this resource
# @param path_name [String] the path partial relative to the resource
# @param options [Hash]
# @option options [String,IO] :content the content for the datastream
# @option options [String] :prefix the prefix for the auto-generated DSID (not to be confused with the solr prefix)
def initialize(digital_object, dsid=nil, options={})
raise ArgumentError, "Digital object is nil" unless digital_object
@digital_object = digital_object
initialize_dsid(dsid, options.delete(:prefix))

#TODO if digital_object.uri is empty, then this resource is not valid:
@uri = if digital_object.uri.kind_of?(RDF::URI) && digital_object.uri.value.empty?
nil
def initialize(parent_or_url = nil, dsid=nil, options={})
case parent_or_url
when nil, String
#TODO this is similar to Core#build_ldp_resource
content = ''
@ldp_source = Ldp::Resource::BinarySource.new(ldp_connection, parent_or_url, content, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path)
when ActiveFedora::Base
#TODO deprecate this path
uri = if parent_or_url.uri.kind_of?(RDF::URI) && parent_or_url.uri.value.empty?
nil
else
"#{parent_or_url.uri}/#{dsid}"
end
@ldp_source = Ldp::Resource::BinarySource.new(ldp_connection, uri, nil, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path)

else
"#{digital_object.uri}/#{@dsid}"
raise "The first argument to #{self} must be a String or an ActiveFedora::Base. You provided a #{parent_or_url.class}"
end

@attributes = {}.with_indifferent_access
unless digital_object.new_record?
@new_record = false
end
end

def resource

end

def ldp_source
@ldp_source ||= Ldp::Resource::BinarySource.new(ldp_connection, uri)
@ldp_source || raise("NO source")
end

def ldp_connection
ActiveFedora.fedora.connection
end

# TODO this is like FedoraAttributes#uri
def uri
ldp_source.subject
end

def new_record?
uri.nil? || ldp_source.new?
ldp_source.new?
end

def digital_object=(digital_object)
raise ArgumentError, "must be a new record to assign a parent object" unless new_record?
@uri = "#{digital_object.uri}/#{@dsid}"
# resource = Ldp::Resource::RdfSource.new(ActiveFedora.fedora.connection, uri)
# init_core(resource)
def uri= uri
@ldp_source = Ldp::Resource::BinarySource.new(ldp_connection, uri, '', ActiveFedora.fedora.host + ActiveFedora.fedora.base_path)
end

# When restoring from previous versions, we need to reload certain attributes from Fedora
def reload
return if new_record?
@ldp_source = nil
@original_name = nil
@mime_type = nil
reset
end

def initialize_dsid(dsid, prefix)
prefix ||= 'DS'
dsid = nil if dsid == ''
dsid ||= generate_dsid(digital_object, prefix)
@dsid = dsid
def reset
@ldp_source = Ldp::Resource::BinarySource.new(ldp_connection, uri)
@original_name = nil
@mime_type = nil
@content = nil
end

def datastream_will_change!
Expand All @@ -87,7 +84,7 @@ def attribute_will_change!(attr)
end
end

def datastream_content
def remote_content
return if new_record?
@ds_content ||= retrieve_content
end
Expand Down Expand Up @@ -190,24 +187,10 @@ def default_mime_type
'text/plain'
end

# return a valid dsid that is not currently in use. Uses a prefix (default "DS") and an auto-incrementing integer
# Example: if there are already datastreams with IDs DS1 and DS2, this method will return DS3. If you specify FOO as the prefix, it will return FOO1.
def generate_dsid(digital_object, prefix)
return unless digital_object
matches = digital_object.attached_files.keys.map {|d| data = /^#{prefix}(\d+)$/.match(d); data && data[1].to_i}.compact
val = matches.empty? ? 1 : matches.max + 1
format_dsid(prefix, val)
end

### Provided so that an application can override how generated ids are formatted (e.g DS01 instead of DS1)
def format_dsid(prefix, suffix)
sprintf("%s%i", prefix,suffix)
end

# The string to prefix all solr fields with. Override this method if you want
# a prefix other than the default
def prefix
"#{dsid.underscore}__"
def prefix(dsid)
dsid ? "#{dsid.underscore}__" : ''
end

def fetch_original_name_from_headers
Expand All @@ -220,10 +203,6 @@ def fetch_mime_type
ldp_source.head.headers['Content-Type']
end

def reset_attributes
@content = nil
end

private

# Rack::Test::UploadedFile is often set via content=, however it's not an IO, though it wraps an io object.
Expand All @@ -244,42 +223,25 @@ def content

def save(*)
return unless content_changed?
raise "Can't generate uri because the parent object isn't saved" if digital_object.new_record?
payload = behaves_like_io?(content) ? content.read : content
headers = { 'Content-Type' => mime_type }
headers['Content-Disposition'] = "attachment; filename=\"#{@original_name}\"" if @original_name
resp = ActiveFedora.fedora.connection.put @uri, payload, headers
reset_attributes
case resp.status
when 201, 204
changed_attributes.clear
when 404
raise ActiveFedora::ObjectNotFoundError, "Unable to add content at #{@container_resource.content_path}"
else
raise "unexpected return value #{resp.status}\n\t#{resp.body[0,200]}"
ldp_source.content = payload
if new_record?
ldp_source.create do |req|
req.headers = headers
end
else
ldp_source.update do |req|
req.headers = headers
end
end
reset
changed_attributes.clear
end

def retrieve_content
return '' if uri.nil?
begin
resp = ActiveFedora.fedora.connection.get(uri)
rescue Ldp::NotFound
return nil
end
case resp.status
when 200, 201
resp.body
when 404
# TODO
# this happens because rdf_datastream calls datastream_content.
# which happens because it needs a ID even though it isn't saved.
# which happens because we don't know if something exists if you give it an id
#raise ActiveFedora::ObjectNotFoundError, "Unable to find content at #{uri}"
''
else
raise "unexpected return value #{resp.status} for when getting datastream content at #{uri}"
end
ldp_source.get.body
end

# @param range [String] the Range HTTP header
Expand All @@ -305,7 +267,7 @@ def stream(range = nil, &block)
def local_or_remote_content(ensure_fetch = true)
return @content if new_record?

@content ||= ensure_fetch ? datastream_content : @ds_content
@content ||= ensure_fetch ? remote_content : @ds_content

if behaves_like_io?(@content)
begin
Expand Down
24 changes: 24 additions & 0 deletions lib/active_fedora/file_path_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module ActiveFedora
class FilePathBuilder
# Builds a relative path for a file
def self.build(digital_object, name, prefix)
name = nil if name == ''
prefix ||= 'DS'
name || generate_dsid(digital_object, prefix)
end

# return a valid dsid that is not currently in use. Uses a prefix (default "DS") and an auto-incrementing integer
# Example: if there are already datastreams with IDs DS1 and DS2, this method will return DS3. If you specify FOO as the prefix, it will return FOO1.
def self.generate_dsid(digital_object, prefix)
return unless digital_object
matches = digital_object.attached_files.keys.map {|d| data = /^#{prefix}(\d+)$/.match(d); data && data[1].to_i}.compact
val = matches.empty? ? 1 : matches.max + 1
format_dsid(prefix, val)
end

### Provided so that an application can override how generated ids are formatted (e.g DS01 instead of DS1)
def self.format_dsid(prefix, suffix)
sprintf("%s%i", prefix,suffix)
end
end
end
4 changes: 2 additions & 2 deletions lib/active_fedora/indexing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def to_solr(solr_doc = Hash.new, opts={})
solr_doc.merge!(SOLR_DOCUMENT_ID.to_sym => id)
solr_doc.merge!(ActiveFedora::Base.profile_solr_name => to_json)
end
attached_files.each_value do |ds|
solr_doc.merge! ds.to_solr()
attached_files.each do |name, ds|
solr_doc.merge! ds.to_solr(solr_doc, name: name )
end
solr_doc = solrize_relationships(solr_doc) unless opts[:model_only]
solr_doc
Expand Down
Loading

1 comment on commit dccebd0

@awead
Copy link
Contributor

@awead awead commented on dccebd0 Nov 7, 2014

Choose a reason for hiding this comment

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

Minor stuff, but I'm all 👍

Please sign in to comment.