lib/duracloud/content.rb in duracloud-client-0.3.0 vs lib/duracloud/content.rb in duracloud-client-0.4.0

- old
+ new

@@ -2,121 +2,96 @@ module Duracloud # # A piece of content in DuraCloud # - class Content - include ActiveModel::Model - include ActiveModel::Dirty - include Persistence - include HasProperties + class Content < AbstractEntity + class CopyError < Error; end + CHUNK_SIZE = 1024 * 16 + COPY_SOURCE_HEADER = "x-dura-meta-copy-source" + COPY_SOURCE_STORE_HEADER = "x-dura-meta-copy-source-store" + MANIFEST_EXT = ".dura-manifest" - after_save :changes_applied - # Does the content exist in DuraCloud? - # @return [Boolean] whether the content exists - # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 attribute - # does not match the stored value - def self.exist?(params={}) - find(params) && true + # @return [Boolean] whether the content exists. + # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option, + # if given, does not match the stored value. + def self.exist?(**kwargs) + find(**kwargs) && true rescue NotFoundError false end # Find content in DuraCloud. # @return [Duraclound::Content] the content - # @raise [Duracloud::NotFoundError] the space, content, or store does not exist. - # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 attribute - # does not match the stored value - def self.find(params={}) - new(params).tap do |content| + # @raise [Duracloud::NotFoundError] the space, content, or store (if given) does not exist. + # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option, + # if given, does not match the stored value. + def self.find(**kwargs) + new(**kwargs).tap do |content| content.load_properties end + rescue NotFoundError => e + ChunkedContent.find(**kwargs) end - attr_accessor :space_id, :content_id, :store_id + # Create new content in DuraCloud. + # @return [Duraclound::Content] the content + # @raise [Duracloud::NotFoundError] the space or store (if given) does not exist. + # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option, + # if given, does not match the stored value. + def self.create(**kwargs) + new(**kwargs).save + end + + attr_accessor :space_id, :content_id, :store_id, + :body, :md5, :content_type alias_method :id, :content_id validates_presence_of :space_id, :content_id - define_attribute_methods :content_type, :body, :md5 - # Return the space associated with this content. # @return [Duracloud::Space] the space. # @raise [Duracloud::NotFoundError] the space or store does not exist. def space - Space.find(space_id, store_id) + @space ||= Space.find(space_id, store_id) end def inspect "#<#{self.class} space_id=#{space_id.inspect}," \ " content_id=#{content_id.inspect}," \ " store_id=#{store_id || '(default)'}>" end - # @api private - # @raise [Duracloud::NotFoundError] the content does not exist in DuraCloud. - def load_body - response = Client.get_content(*args, **query) - set_md5!(response) - @body = response.body # don't use setter b/c marks as dirty - persisted! - end - - def load_properties - super do |response| - # don't mark content_type or md5 as changed - set_md5!(response) - @content_type = response.content_type - end - end - - def body=(str_or_io) - @body = str_or_io - body_will_change! - end - - # Return the content body, loading from DuraCloud if necessary. - # @return [String, StringIO] the content body - def body - load_body if persisted? && empty? - @body - end - # Is the content empty? # @return [Boolean] whether the content is empty (nil or empty string) def empty? - @body.nil? || @body.size == 0 + body.nil? || ( body.respond_to?(:size) && body.size == 0 ) end - def content_type=(val) - content_type_will_change! unless val == @content_type - @content_type = val + # Downloads the remote content + # @yield [String] chunk of the remote content, if block given. + # @return [Duracloud::Response] the response to the content request. + # @raise [Duracloud::NotFoundError] + def download(&block) + Client.get_content(*args, **query, &block) end - def content_type - @content_type - end - - def md5=(val) - md5_will_change! unless val == @md5 - @md5 = val - end - - def md5 - @md5 - end - # @return [Duracloud::Content] the copied content # The current instance still represents the original content. - def copy(target_space_id:, target_content_id:, target_store_id: nil) - copy_headers = {'x-dura-meta-copy-source'=>[space_id, content_id].join('/')} - copy_headers['x-dura-meta-copy-source-store'] = store_id if store_id - options = { storeID: target_store_id, headers: copy_headers } - Client.copy_content(target_space_id, target_content_id, **options) - Content.find(space_id: target_space_id, content_id: target_content_id, store_id: target_store_id, md5: md5) + def copy(**args) + dest = args.except(:force) + dest[:space_id] ||= space_id + dest[:content_id] ||= content_id + raise CopyError, "Destination is the same as the source." if dest == copy_source + if !args[:force] && Content.exist?(**dest) + raise CopyError, "Destination exists and :false option is false." + end + options = { storeID: dest[:store_id], headers: copy_headers } + Client.copy_content(dest[:space_id], dest[:content_id], **options) + Content.new(dest.merge(md5: md5)) end # @return [Duracloud::Content] the moved content # The current instance still represents the deleted content. def move(**args) @@ -125,42 +100,38 @@ copied end private - def set_md5!(response) - if md5 - if md5 != response.md5 - raise MessageDigestError, - "Expected MD5 digest (#{md5}) does not match response header: #{response.md5}" - end - else - @md5 = response.md5 - end - end - - def io_like? - body.respond_to?(:read) && body.respond_to?(:rewind) - end - - def set_properties - headers = properties.to_h - headers["Content-Type"] = content_type if content_type_changed? - options = { headers: headers, query: query } - Client.set_content_properties(*args, **options) - end - def store headers = { "Content-MD5" => md5 || calculate_md5, "Content-Type" => content_type || "application/octet-stream" } headers.merge!(properties) options = { body: body, headers: headers, query: query } Client.store_content(*args, **options) end + def copy_headers + ch = { COPY_SOURCE_HEADER=>"#{space_id}/#{content_id}" } + ch[COPY_SOURCE_STORE_HEADER] = store_id if store_id + ch + end + + def copy_source + { space_id: space_id, content_id: content_id, store_id: store_id } + end + + def io_like? + body.respond_to?(:read) && body.respond_to?(:rewind) + end + + def set_properties + Client.set_content_properties(*args, headers: properties, query: query) + end + def calculate_md5 digest = Digest::MD5.new if io_like? body.rewind while chunk = body.read(CHUNK_SIZE) do @@ -175,19 +146,28 @@ def properties_class ContentProperties end - def get_properties_response - Client.get_content_properties(*args, **query) + def do_load_properties + response = Client.get_content_properties(*args, **query) + if md5 + if md5 != response.md5 + raise MessageDigestError, "Expected MD5: {#{md5}}; DuraCloud MD5: {#{response.md5}}." + end + else + self.md5 = response.md5 + end + self.properties = response.headers + self.content_type = response.content_type end def do_delete Client.delete_content(*args, **query) end def do_save - if !empty? && body_changed? + if !empty? store elsif persisted? set_properties else raise Error, "Cannot store empty content."