# Representation of an object stored in a bucket. module S33r class S3Object < Client attr_accessor :key, :last_modified, :etag, :size, :owner, :storage_class, :value, :content_type, :render_as_attachment # Name of bucket this object is attached to. attr_reader :bucket # Metadata to set with x-amz-meta- style headers. Note that the bit after x-amz-meta- # is stored for each key, rather than the full key. attr_accessor :amz_meta alias :meta :amz_meta # +options+ can include: # * :bucket => Bucket: Bucket this object is attached to. # * :metadata => Hash: metadata to use in building the object. # * :amz_meta => Hash: metadata specific to Amazon. def initialize(key, value=nil, options={}) @key = key @value = value @content_type = 'text/plain' @render_as_attachment = false @amz_meta = options[:amz_meta] || {} set_bucket(options[:bucket]) metadata = options[:metadata] || {} set_properties(metadata) unless metadata.empty? end # Set a bucket instance as the default bucket for this object. def set_bucket(bucket_instance) if bucket_instance @bucket = bucket_instance set_options(@bucket.settings) extend(InBucket) end end alias :bucket= :set_bucket # Set the properties of the object from some metadata name-value pairs. # # +metadata+ is a hash of properties and their values, used to set the # corresponding properties on the object. def set_properties(metadata) # required properties @etag = metadata[:etag].gsub("\"", "") if metadata[:etag] @last_modified = Time.parse(metadata[:last_modified]) if metadata[:last_modified] @size = metadata[:size].to_i if metadata[:size] @render_as_attachment = metadata[:render_as_attachment] || false # only set if creating object from XML (not available otherwise) @owner ||= metadata[:owner] # only set if creating object from HTTP response @content_type = metadata[:content_type] if metadata[:content_type] end # To create an object which reads the content in from a file; # you can then save the object to its associated bucket (if you like). def self.from_file(filename, options={}) mime_type = guess_mime_type(filename) content_type = mime_type.simplified value = File.open(filename).read key = options[:key] || filename options.merge!(:metadata => {:content_type => content_type}) self.new(key, value, options) end # Create a new instance from a string. def self.from_text(key, text, options={}) content_type = 'text/plain' options.merge!(:metadata => {:content_type => content_type}) self.new(key, text, options) end # Set properties of the object from an XML string. # # +xml_str+ should be a string representing a full XML document, # containing a element as its root element. def self.from_xml_string(xml_str, options={}) self.from_xml_node(XML.get_xml_doc(xml_str)) end # Create a new instance from an XML document; # N.B. this instance will have no value associated with it (yet). # Call the load method to populate it. # # +options+ is passed to the constructor. def self.from_xml_node(doc, options={}) metadata = self.parse_xml_node(doc) options.merge!(:metadata => metadata) self.new(metadata[:key], nil, options) end # Get properties of the object from an XML document, e.g. as returned in a bucket listing. # # +doc+: XML::Document instance to parse to get properties for this object. # # Returns the metadata relating to the object, as stored on S3. def self.parse_xml_node(doc) metadata = {} metadata[:key] = doc.xget('Key') metadata[:last_modified] = doc.xget('LastModified') metadata[:etag] = doc.xget('ETag') metadata[:size] = doc.xget('Size') # Build representation of the owner. user_xml_doc = doc.find('Owner').to_a.first metadata[:owner] = S3ACL::CanonicalUser.from_xml(user_xml_doc) metadata end # Create a new instance from a HTTP response. # This is useful if you do a GET for a resource key and # want to convert the response into an object; NB the response # doesn't necessarily contain all the metadata you might want - you need to # do a HEAD for that. # # +key+ is the key for the resource (not part of the response). # +resp+ is a Net::HTTPResponse instance to parse. # +options+ is passed through to the constructor (see initialize). # # Note that if the resp returns nil, a blank object is created. def self.from_response(key, resp, options={}) result = self.parse_response(resp) if result metadata, amz_meta, value = result options.merge!(:metadata => metadata, :amz_meta => amz_meta) else value = nil end self.new(key, value, options) end # Parse the response returned by GET on a resource key # within a bucket. # # +resp+ is a Net::HTTPResponse instance. # # Returns an array [+metadata+, +response.body+]; or nil if the object # doesn't exist. def self.parse_response(resp) resp_headers = resp.to_hash # If there's no etag, there's no content in the resource. if resp_headers['etag'] metadata = {} metadata[:last_modified] = resp_headers['last-modified'][0] metadata[:etag] = resp_headers['etag'][0] metadata[:size] = resp_headers['content-length'][0] metadata[:content_type] = resp_headers['content-type'][0] content_disposition = resp_headers['content-disposition'] if content_disposition content_disposition = content_disposition[0] if /^attachment/ =~ content_disposition metadata[:render_as_attachment] = true end end # x-amz-meta- response headers. interesting_header = Regexp.new(METADATA_PREFIX) new_amz_meta = {} resp.each_header do |key, value| new_amz_meta[key.gsub(interesting_header, '')] = value if interesting_header =~ key end # The actual content of the S3 object. value = resp.body [metadata, new_amz_meta, value] else nil end end # Set metadata on the object. def []=(key, value) amz_meta[key] = value end # Get metadata on the object. def [](key) amz_meta[key] end end module InBucket # Send requests using the bucket's options. def request_defaults bucket.request_defaults.merge(:key => key) end # Fetch an object's metadata and content from S3. def fetch(options={}) resp = do_get(options) metadata, amz_meta, data = S3Object.parse_response(resp) @amz_meta = amz_meta @value = data set_properties(metadata) true end # Save an object to S3. def save(options={}) bucket.put(self, options) end # Delete the object from S3. def delete(options={}) bucket.delete(key, options) end # Change the object's name on S3. def rename(new_key, options={}) # Delete the object from S3. bucket.delete(key, options) # Set the new key. self.key = new_key options[:key] = new_key save(options) end alias :mv :rename end end