# 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