Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1192 lines (1064 sloc) 40.741 kb
# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
require 'rexml/document'
require 'pathname'
require 'stringio'
require 'json'
require 'digest/md5'
module AWS
class S3
##
# Provides a low-level client to Amazon S3:
#
# * Each method makes exactly one request to S3, and no two
# methods make the same type of request.
#
# * These methods hide the details of how request parameters are
# sent to S3; for example:
#
# client.set_bucket_acl(# controls which host to connect to
# :bucket_name => "mybucket",
# # the request payload
# :acl => [{ :grantee => "..." }])
#
# * These methods return subclasses of Response, so that you can
# always get access to the request that was made and the raw
# HTTP response. You can also access S3-specific response
# metadata. For example:
#
# response = client.list_buckets
# response.http_request.http_method # => "GET"
# response.http_response.body # => "<ListAllMyBucketsResult xmlns..."
# response.request_id # => "32FE2CEB32F5EE25"
# # (S3-specific metadata)
#
# * This client attempts to raise ArgumentError for any invalid
# requests it can detect before sending a request to the
# service. For example:
#
# begin
# client.create_bucket
# rescue ArgumentError => e
# puts e # prints "The bucket_name parameter is
# # required"
# end
#
# * Each method can take an +:async+ to turn it into an
# asynchronous operation. Instead of blocking on the response
# to the service call, the method will return a handle on the
# response. For example:
#
# response = client.list_buckets(:async => true)
# response.on_success { p response.buckets.map(&:name) }
#
# @private
class Client < Core::Client
AWS.register_autoloads(self) do
autoload :XML, 'xml'
end
API_VERSION = '2006-03-01'
XMLNS = "http://s3.amazonaws.com/doc/#{API_VERSION}/"
EMPTY_BODY_ERRORS = {
304 => Errors::NotModified,
404 => Errors::NoSuchKey
}
include DataOptions
include Core::UriEscape
configure_client
protected
def self.bucket_method(method_name, verb, *args, &block)
method_options = (args.pop if args.last.kind_of?(Hash)) || {}
xml_grammar = (args.pop if args.last.respond_to?(:parse))
verb = verb.to_s.upcase
subresource = args.first
add_client_request_method(method_name, :xml_grammar => xml_grammar) do
configure_request do |req, options|
require_bucket_name!(options[:bucket_name])
req.http_method = verb
req.bucket = options[:bucket_name]
req.add_param(subresource) if subresource
if header_options = method_options[:header_options]
header_options.each do |(option_name, header)|
req.headers[header] = options[option_name] if
options[option_name]
end
end
end
instance_eval(&block) if block
end
end
protected
def self.object_method(method_name, verb, *args, &block)
bucket_method(method_name, verb, *args) do
configure_request do |req, options|
validate_key!(options[:key])
super(req, options)
req.key = options[:key]
end
instance_eval(&block) if block
end
end
public
bucket_method(:create_bucket, :put) do
configure_request do |req, options|
validate_bucket_name!(options[:bucket_name])
req.canned_acl = options[:acl]
if location = options[:location_constraint]
xmlns = "http://s3.amazonaws.com/doc/#{API_VERSION}/"
req.body = <<-XML
<CreateBucketConfiguration xmlns="#{xmlns}">
<LocationConstraint>#{location}</LocationConstraint>
</CreateBucketConfiguration>
XML
end
super(req, options)
end
end
##
# Deletes a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
bucket_method(:delete_bucket, :delete)
bucket_method(:set_bucket_lifecycle_configuration, :put) do
configure_request do |req, options|
xml = options[:lifecycle_configuration]
md5 = Base64.encode64(Digest::MD5.digest(xml)).strip
req.add_param('lifecycle')
req.body = xml
req.headers['content-md5'] = md5
super(req, options)
end
end
bucket_method(:get_bucket_lifecycle_configuration, :get) do
configure_request do |req, options|
req.add_param('lifecycle')
super(req, options)
end
process_response do |resp|
xml = resp.http_response.body
data = XML::GetBucketLifecycleConfiguration.parse(xml)
Core::MetaUtils.extend_method(resp, :data) { ResponseData.new(data) }
end
end
bucket_method(:delete_bucket_lifecycle_configuration, :delete) do
configure_request do |req, options|
req.add_param('lifecycle')
super(req, options)
end
end
# delete_bucket_lifecycle_configuration
##
# Lists the buckets in the account.
add_client_request_method(:list_buckets) do
configure_request do |req, options|
req.http_method = "GET"
end
process_response do |resp|
XML::ListBuckets.parse(resp.http_response.body, :context => resp)
end
end
##
# Sets the access policy for a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# * +:policy+ -- The new policy. This can be a string (which
# is assumed to contain a valid policy expressed in JSON), a
# Policy or any object that responds to +to_json+.
#
# == Response
#
# The response contains only the standard fields.
bucket_method(:set_bucket_policy, :put, 'policy') do
configure_request do |req, options|
require_policy!(options[:policy])
super(req, options)
policy = options[:policy]
policy = policy.to_json unless policy.respond_to?(:to_str)
req.body = policy
end
end
##
# Gets the access policy for a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# == Response
#
# A successful response will have a +policy+ method that
# returns an instance of Policy.
#
bucket_method(:get_bucket_policy, :get, 'policy') do
process_response do |resp|
# FIXME: this makes unit testing easier, but is there something
# we should be doing in case of invalid JSON from the service?
policy = Policy.from_json(resp.http_response.body) rescue nil
Core::MetaUtils.extend_method(resp, :policy) { policy }
end
end
##
# Deletes the access policy for a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
bucket_method(:delete_bucket_policy, :delete, 'policy')
bucket_method(:set_bucket_versioning, :put, 'versioning') do
configure_request do |req, options|
state = options[:state].to_s.downcase.capitalize
unless state =~ /^(Enabled|Suspended)$/
raise ArgumentError, "invalid versioning state `#{state}`"
end
super(req, options)
req.body = <<-XML.strip
<VersioningConfiguration xmlns="#{XMLNS}">
<Status>#{state}</Status>
</VersioningConfiguration>
XML
end
end
##
# Gets the bucket's location constraint.
# @return [String] The bucket location constraint. Returns nil if
# the bucket was created in the US classic region.
bucket_method(:get_bucket_location, :get, 'location') do
process_response do |response|
regex = />(.*)<\/LocationConstraint>/
matches = response.http_response.body.match(regex)
location = matches ? matches[1] : nil
Core::MetaUtils.extend_method(response, :location_constraint) { location }
end
end
bucket_method(:get_bucket_versioning, :get, 'versioning',
XML::GetBucketVersioning)
bucket_method(:list_object_versions, :get, 'versions',
XML::ListObjectVersions) do
configure_request do |req, options|
super(req, options)
params = %w(delimiter key_marker max_keys prefix version_id_marker)
params.each do |param|
if options[param.to_sym]
req.add_param(param.gsub(/_/, '-'), options[param.to_sym])
end
end
end
end
##
# Sets the access control list for a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# * +:acl+ -- The new acl. This can be any of the following:
# * An XML policy as a string (which is passed to S3 uninterpreted)
# * An AccessControlList object
# * Any object that responds to +to_xml+
# * Any Hash that is acceptable as an argument to
# AccessControlList#initialize.
#
# == Response
#
# The response contains only the standard fields.
bucket_method(:set_bucket_acl, :put, 'acl') do
configure_request do |req, options|
require_acl!(options[:acl])
super(req, options)
if options[:acl].kind_of?(Hash)
req.body = AccessControlList.new(options[:acl]).to_xml
elsif options[:acl].respond_to?(:to_str)
req.body = options[:acl]
else
req.body = options[:acl].to_xml
end
end
end
##
# Gets the access control list for a bucket.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# == Response
#
# A successful response will have an +acl+ method that
# returns an instance of AccessControlList.
#
bucket_method(:get_bucket_acl, :get, 'acl',
XML::GetBucketAcl)
##
# Sets the access control list for an object.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# * +:key+ -- The key of the object.
#
# * +:acl+ -- The new acl. This can be a string (which is
# assumed to contain a valid ACL expressed in XML), a
# AccessControlList or any object whose +to_xml+ returns a
# valid ACL expressed in XML.
#
# == Response
#
# The response contains only the standard fields.
object_method(:set_object_acl, :put, 'acl') do
configure_request do |req, options|
require_acl!(options[:acl]) unless options[:acl].kind_of?(Symbol)
super(req, options)
if options[:acl].kind_of?(Hash)
req.body = AccessControlList.new(options[:acl]).to_xml
elsif options[:acl].kind_of?(Symbol)
req.headers["x-amz-acl"] = options[:acl].to_s.tr("_","-")
elsif options[:acl].respond_to?(:to_str)
req.body = options[:acl]
else
req.body = options[:acl].to_xml
end
end
end
##
# Gets the access control list for an object.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket.
#
# * +:key+ -- The key of the object.
#
# == Response
#
# A successful response will have an +acl+ method that
# returns an instance of AccessControlList.
object_method(:get_object_acl, :get, 'acl',
XML::GetBucketAcl)
##
# Puts data into an object, replacing the current contents.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket that will contain the data.
#
# * +:key+ -- The key under which the data will be saved.
#
# * +:data+ -- The data to upload. This can be provided as an option
# or when using block form (see below). Valid values include:
#
# * A string
#
# * A Pathname object.
#
# * Any object with +read+ and +eof?+ methods that behave
# like Ruby's IO class (e.g. IO, File, Tempfile, StringIO, etc).
# The object must support the following access methods:
#
# read # all at once
# read(length) until eof? # in chunks
#
# == Optional
#
# * +:content_length+ -- Required if you are using block form to
# write data or if it is not possible to determine the size of
# +:data+. Best effort is made to determine the content length of
# strings, files, tempfiles, io objects, and any object that responds
# to #length or #size.
#
# * +:metadata+ -- A hash of metadata to be included with the
# object. These will be sent to S3 as headers prefixed with
# +x-amz-meta+.
#
# * +:acl+ -- A canned access control policy, valid values are:
# * +:private+
# * +:public_read+
# * ...
# Defaults to +:private+
#
# * +:storage_class+ -- Controls whether Reduced Redundancy
# Storage is enabled for the object. Valid values are
# +:standard+ (the default) or +:reduced_redundancy+
#
# * +:cache_control+ -- Can be used to specify caching
# behavior [...]
#
# * +:content_disposition+ -- Specifies presentational
# information [...]
#
# * +:content_encoding+ -- Specifies what content encodings
# have been [...]
#
# * +:content_md5+ -- The base64 encoded 128-bit [...]
#
# * +:content_type+ -- A standard MIME type describing [...]
#
# == Block Form
#
# In block form, this method yields a stream to the block that
# accepts data chunks. For example:
#
# s3_client.put_object(
# :bucket_name => 'mybucket',
# :key => 'some/key'
# :content_length => File.size('myfile')
# ) do |buffer|
#
# File.open('myfile') do |io|
# buffer.write(io.read(length)) until io.eof?
# end
#
# end
#
# This form is useful if you need finer control over how
# potentially large amounts of data are read from another
# source before being sent to S3; for example, if you are
# using a non-blocking IO model and reading from a large file
# on disk or from another network stream. Some HTTP handlers
# do not support streaming request bodies, so if you plan to
# upload large objects using this interface you should make
# sure the HTTP handler you configure for the client meets
# your needs.
#
# == Response
#
# If bucket versioning is enabled, a successful response will
# have a +version_id+ method that returns the version ID of
# the version that was written in the request.
#
object_method(:put_object, :put,
:header_options => {
:content_md5 => 'Content-MD5',
:cache_control => 'Cache-Control',
:content_disposition => 'Content-Disposition',
:content_encoding => 'Content-Encoding',
:content_type => 'Content-Type',
:storage_class => 'x-amz-storage-class',
:server_side_encryption => 'x-amz-server-side-encryption',
:expires => 'Expires'
}) do
configure_request do |request, options, block|
options[:server_side_encryption] =
options[:server_side_encryption].to_s.upcase if
options[:server_side_encryption].kind_of?(Symbol)
super(request, options)
set_request_data(request, options, block)
request.metadata = options[:metadata]
request.canned_acl = options[:acl]
request.storage_class = options[:storage_class]
end
process_response do |response|
Core::MetaUtils.extend_method(response, :version_id) do
response.http_response.header('x-amz-version-id')
end
Core::MetaUtils.extend_method(response, :etag) do
response.http_response.header('ETag')
end
Core::MetaUtils.extend_method(response, :last_modified) do
Time.parse(response.http_response.header('Last-Modified'))
end
add_sse_to_response(response)
end
simulate_response do |response|
Core::MetaUtils.extend_method(response, :etag) { "abc123" }
Core::MetaUtils.extend_method(response, :version_id) { nil }
end
end
##
# Gets the data for a key.
#
# == Required Options
#
# * +:bucket_name+ -- The name of the bucket that contains the data.
#
# * +:key+ -- The key under which the data exists.
#
# == Optional
#
# * +:if_modified_since+ -- A Time object; if specified, the
# response will contain an additional +modified?+ method that
# returns true if the object was modified after the given
# time. If +modified?+ returns false, the +data+ method of
# the response will return +nil+.
#
# * +:if_unmodified_since+ -- A Time object; if specified, the
# response will contain an additional +unmodified?+ method
# that returns true if the object was not modified after the
# given time. If +unmodified?+ returns false, the +data+
# method of the response will return +nil+.
#
# * +:if_match+ -- A string; if specified, the response will
# contain an additional +matches?+ method that returns true
# if the object ETag matches the value for this option. If
# +matches?+ returns false, the +data+ method of the
# response will return +nil+.
#
# * +:if_none_match+ -- A string; if specified, the response
# will contain an additional +matches?+ method that returns
# true if and only if the object ETag matches the value for
# this option. If +matches?+ returns true, the +data+
# method of the response will return +nil+.
#
# * +:to+ -- A destination for the data. Valid values:
#
# * The path to a file as a string
#
# * A Pathname object
#
# * Any object that supports <code>write(data)</code> and
# +close+ methods like Ruby's IO class
#
# * +:range+ -- TODO: figure out the format for this
# parameter.
#
# == Response
#
# A successful response will have some combination of the
# following methods:
#
# * +data+ -- The object data as a string. This will return
# +nil+ if one of the conditional options above is specified
# and the condition is not met. It will also return +nil+
# if +deleted?+ returns true. It will not be present if the
# +:to+ option is specified.
#
# * +modified?+, +unmodified?+, +matches?+ -- These will be
# present as documented in the conditional options above.
#
# * +version_id+ -- Returns the version ID of the object that
# was written (only for versioned buckets).
#
# * +deleted?+ -- This will return +true+ if the bucket has
# versioning enabled and the object retrieved was a delete
# marker.
object_method(:get_object, :get,
:header_options => {
:if_modified_since => "If-Modified-Since",
:if_unmodified_since => "If-Unmodified-Since",
:if_match => "If-Match",
:if_none_match => "If-None-Match"
}) do
configure_request do |req, options|
super(req, options)
if options[:version_id]
req.add_param('versionId', options[:version_id])
end
["If-Modified-Since",
"If-Unmodified-Since"].each do |date_header|
case value = req.headers[date_header]
when DateTime
req.headers[date_header] = Time.parse(value.to_s).rfc2822
when Time
req.headers[date_header] = value.rfc2822
end
end
if options[:range]
range = options[:range]
range = "bytes=#{range.first}-#{range.last}" if range.is_a?(Range)
req.headers['Range'] = range
end
end
process_response do |resp|
Core::MetaUtils.extend_method(resp, :data) { resp.http_response.body }
Core::MetaUtils.extend_method(resp, :version_id) do
http_response.header('x-amz-version-id')
end
add_sse_to_response(resp)
end
end
object_method(:head_object, :head) do
configure_request do |req, options|
super(req, options)
if options[:version_id]
req.add_param('versionId', options[:version_id])
end
end
process_response do |resp|
# create a hash of user-supplied metadata
Core::MetaUtils.extend_method(resp, :meta) do
meta = {}
resp.http_response.headers.each_pair do |name,value|
if name =~ /^x-amz-meta-(.+)$/i
meta[$1] = [value].flatten.join
end
end
meta
end
if expiry = resp.http_response.headers['x-amz-expiration']
expiry.first =~ /^expiry-date="(.+)", rule-id="(.+)"$/
date = DateTime.parse($1)
rule_id = $2
else
date = nil
rule_id = nil
end
Core::MetaUtils.extend_method(resp, :expiration_date) { date }
Core::MetaUtils.extend_method(resp, :expiration_rule_id) { rule_id }
# create methods for standard response headers
{
'x-amz-version-id' => :version_id,
'content-type' => :content_type,
'etag' => :etag,
}.each_pair do |header,method|
Core::MetaUtils.extend_method(resp, method) do
http_response.header(header)
end
end
Core::MetaUtils.extend_method(resp, :last_modified) do
Time.parse(resp.http_response.header('Last-Modified'))
end
Core::MetaUtils.extend_method(resp, :content_length) do
http_response.header('content-length').to_i
end
add_sse_to_response(resp)
end
end
object_method(:delete_object, :delete) do
configure_request do |req, options|
super(req, options)
if options[:version_id]
req.add_param('versionId', options[:version_id])
end
end
process_response do |resp|
Core::MetaUtils.extend_method(resp, :version_id) do
http_response.header('x-amz-version-id')
end
end
end
bucket_method(:list_objects, :get, XML::ListObjects) do
configure_request do |req, options|
super(req, options)
params = %w(delimiter marker max_keys prefix)
params.each do |param|
if options[param.to_sym]
req.add_param(param.gsub(/_/, '-'), options[param.to_sym])
end
end
end
end
object_method(:initiate_multipart_upload, :post, 'uploads',
XML::InitiateMultipartUpload,
:header_options => {
:cache_control => 'Cache-Control',
:content_disposition => 'Content-Disposition',
:content_encoding => 'Content-Encoding',
:content_type => 'Content-Type',
:storage_class => 'x-amz-storage-class',
:server_side_encryption => 'x-amz-server-side-encryption',
:expires => 'Expires'
}) do
configure_request do |req, options|
options[:server_side_encryption] =
options[:server_side_encryption].to_s.upcase if
options[:server_side_encryption].kind_of?(Symbol)
super(req, options)
req.metadata = options[:metadata]
req.canned_acl = options[:acl]
req.storage_class = options[:storage_class]
end
process_response do |response|
add_sse_to_response(response)
end
end
bucket_method(:list_multipart_uploads,
:get, 'uploads',
XML::ListMultipartUploads) do
configure_request do |req, options|
super(req, options)
params = %w(delimiter key_marker max_keys) +
%w(upload_id_marker max_uploads prefix)
params.each do |param|
if options[param.to_sym]
req.add_param(param.gsub(/_/, '-'), options[param.to_sym])
end
end
end
end
bucket_method(:delete_objects, :post, 'delete', XML::DeleteObjects) do
configure_request do |req, options|
super(req, options)
quiet = options.key?(:quiet) ? options[:quiet] : true
objects = options[:objects].inject('') do |xml,o|
xml << "<Object><Key>#{o[:key]}</Key>"
xml << "<VersionId>#{o[:version_id]}</VersionId>" if o[:version_id]
xml << "</Object>"
end
xml = '<?xml version="1.0" encoding="UTF-8"?>'
xml << "<Delete><Quiet>#{quiet}</Quiet>#{objects}</Delete>"
req.body = xml
md5 = Base64.encode64(Digest::MD5.digest(xml)).strip
req.headers['content-md5'] = md5
end
end
object_method(:upload_part, :put,
:header_options => {
:content_md5 => 'Content-MD5'
}) do
configure_request do |request, options, block|
require_upload_id!(options[:upload_id])
validate!("part_number", options[:part_number]) do
"must not be blank" if options[:part_number].to_s.empty?
end
super(request, options)
set_request_data(request, options, block)
request.add_param('uploadId', options[:upload_id])
request.add_param('partNumber', options[:part_number])
end
process_response do |response|
Core::MetaUtils.extend_method(response, :etag) do
response.http_response.header('ETag')
end
Core::MetaUtils.extend_method(response, :last_modified) do
Time.parse(response.http_response.header('Last-Modified'))
end
add_sse_to_response(response)
end
simulate_response do |response|
Core::MetaUtils.extend_method(response, :etag) { "abc123" }
end
end
object_method(:complete_multipart_upload, :post,
XML::CompleteMultipartUpload) do
configure_request do |req, options|
require_upload_id!(options[:upload_id])
validate_parts!(options[:parts])
super(req, options)
req.add_param('uploadId', options[:upload_id])
parts_xml = options[:parts].map do |part|
"<Part>"+
"<PartNumber>#{part[:part_number].to_i}</PartNumber>"+
"<ETag>#{REXML::Text.normalize(part[:etag].to_s)}</ETag>"+
"</Part>"
end.join
req.body =
"<CompleteMultipartUpload>#{parts_xml}</CompleteMultipartUpload>"
end
process_response do |response|
Core::MetaUtils.extend_method(response, :version_id) do
response.http_response.header('x-amz-version-id')
end
add_sse_to_response(response)
end
simulate_response do |response|
Core::MetaUtils.extend_method(response, :version_id) { nil }
end
end
object_method(:abort_multipart_upload, :delete) do
configure_request do |req, options|
require_upload_id!(options[:upload_id])
super(req, options)
req.add_param('uploadId', options[:upload_id])
end
end
object_method(:list_parts, :get,
XML::ListParts) do
configure_request do |req, options|
require_upload_id!(options[:upload_id])
super(req, options)
req.add_param('uploadId', options[:upload_id])
req.add_param('max-parts', options[:max_parts])
req.add_param('part-number-marker', options[:part_number_marker])
end
end
##
# @param [Hash] options
# @option options [required, String] :bucket_name Name of the bucket
# to copy a object into.
# @option options [required, String] :key Where (object key) in the
# bucket the object should be copied to.
# @option options [required, String] :copy_source The name of the
# source bucket and key name of the source object, separated by a
# slash (/). This string must be URL-encoded. Additionally, the
# source bucket must be valid and you must have READ access to
# the valid source object.
# @option options [Symbol] :acl
#
object_method(:copy_object, :put,
:header_options => {
:copy_source => 'x-amz-copy-source',
:cache_control => 'Cache-Control',
:metadata_directive => 'x-amz-metadata-directive',
:storage_class => 'x-amz-storage-class',
:server_side_encryption => 'x-amz-server-side-encryption',
:content_type => 'Content-Type',
}) do
configure_request do |req, options|
# TODO : validate metadata directive COPY / REPLACE
# TODO : validate storage class STANDARD / REDUCED_REDUNDANCY
# TODO : add validations for storage class in other places used
validate!(:copy_source, options[:copy_source]) do
"may not be blank" if options[:copy_source].to_s.empty?
end
options = options.merge(:copy_source => escape_path(options[:copy_source]))
options[:server_side_encryption] =
options[:server_side_encryption].to_s.upcase if
options[:server_side_encryption].kind_of?(Symbol)
super(req, options)
req.canned_acl = options[:acl]
req.metadata = options[:metadata]
req.storage_class = options[:storage_class]
if options[:version_id]
req.headers['x-amz-copy-source'] += "?versionId=#{options[:version_id]}"
end
end
process_response do |response|
Core::MetaUtils.extend_method(response, :version_id) do
response.http_response.header('x-amz-version-id')
end
Core::MetaUtils.extend_method(response, :etag) do
response.http_response.header('ETag')
end
Core::MetaUtils.extend_method(response, :last_modified) do
Time.parse(response.http_response.header('Last-Modified'))
end
add_sse_to_response(response)
end
end
protected
def extract_error_code response
if (response.http_response.status >= 300 ||
response.request_type == :complete_multipart_upload) and
body = response.http_response.body and
parse = Core::XmlGrammar.parse(body) and
parse.respond_to?(:code)
parse.code
end
end
protected
def populate_error response
code = response.http_response.status
if EMPTY_BODY_ERRORS.include?(code) and
response.http_response.body.nil?
response.error =
EMPTY_BODY_ERRORS[code].new(response.http_request,
response.http_response)
else
super
end
end
protected
def should_retry? response
super or
response.request_type == :complete_multipart_upload &&
extract_error_code(response)
end
protected
def set_request_data request, options, block
request.body_stream = data_stream_from(options, &block)
request.headers['Content-Length'] = content_length_from(options)
end
protected
def new_request
S3::Request.new
end
protected
def add_sse_to_response(response)
sse = nil
if value = response.http_response.header('x-amz-server-side-encryption')
sse = value.downcase.to_sym
end
Core::MetaUtils.extend_method(response, :server_side_encryption) { sse }
end
module Validators
# Returns true if the given bucket name is valid.
def valid_bucket_name?(bucket_name)
validate_bucket_name!(bucket_name) rescue false
end
# Returns true if the given +bucket_name+ is DNS compatible.
#
# DNS compatible bucket names may be accessed like:
#
# http://dns.compat.bucket.name.s3.amazonaws.com/
#
# Whereas non-dns compatible bucket names must place the bucket
# name in the url path, like:
#
# http://s3.amazonaws.com/dns_incompat_bucket_name/
#
# @return [Boolean] Returns true if the given bucket name may be
# is dns compatible.
# this bucket n
def dns_compatible_bucket_name?(bucket_name)
return false if
!valid_bucket_name?(bucket_name) or
# Bucket names should not contain underscores (_)
bucket_name["_"] or
# Bucket names should be between 3 and 63 characters long
bucket_name.size > 63 or
# Bucket names should not end with a dash
bucket_name[-1,1] == '-' or
# Bucket names cannot contain two, adjacent periods
bucket_name['..'] or
# Bucket names cannot contain dashes next to periods
# (e.g., "my-.bucket.com" and "my.-bucket" are invalid)
(bucket_name['-.'] || bucket_name['.-'])
true
end
# Returns true if the bucket name must be used in the request
# path instead of as a sub-domain when making requests against
# S3.
#
# This can be an issue if the bucket name is DNS compatible but
# contains '.' (periods). These cause the SSL certificate to
# become invalid when making authenticated requets over SSL to the
# bucket name. The solution is to send this as a path argument
# instead.
#
# @return [Boolean] Returns true if the bucket name should be used
# as a path segement instead of dns prefix when making requests
# against s3.
def path_style_bucket_name? bucket_name
if dns_compatible_bucket_name?(bucket_name)
bucket_name =~ /\./ ? true : false
else
true
end
end
protected
def validate! name, value, &block
if error_msg = yield
raise ArgumentError, "#{name} #{error_msg}"
end
value
end
protected
def validate_key!(key)
validate!('key', key) do
case
when key.nil? || key == ''
'may not be blank'
end
end
end
protected
def require_bucket_name! bucket_name
if [nil, ''].include?(bucket_name)
raise ArgumentError, "bucket_name may not be blank"
end
end
# Returns true if the given bucket name is valid. If the name
# is invalid, an ArgumentError is raised.
protected
def validate_bucket_name!(bucket_name)
validate!('bucket_name', bucket_name) do
case
when bucket_name.nil? || bucket_name == ''
'may not be blank'
when bucket_name !~ /^[a-z0-9._\-]+$/
'may only contain lowercase letters, numbers, periods (.), ' +
'underscores (_), and dashes (-)'
when bucket_name !~ /^[a-z0-9]/
'must start with a letter or a number'
when !(3..255).include?(bucket_name.size)
'must be between 3 and 255 characters long'
when bucket_name =~ /(\d+\.){3}\d+/
'must not be formatted like an IP address (e.g., 192.168.5.4)'
when bucket_name =~ /\n/
'must not contain a newline character'
end
end
end
protected
def require_policy!(policy)
validate!('policy', policy) do
case
when policy.nil? || policy == ''
'may not be blank'
else
json_validation_message(policy)
end
end
end
protected
def require_acl!(acl)
validate!('acl', acl) do
case
when acl.kind_of?(Hash)
AccessControlList.new(acl).validate!
nil
when !acl.respond_to?(:to_str) && !acl.respond_to?(:to_xml)
"must support to_xml: #{acl.inspect}"
when acl.nil? || acl == ''
'may not be blank'
else
xml_validation_message(acl)
end
end
end
protected
def require_upload_id!(upload_id)
validate!("upload_id", upload_id) do
"must not be blank" if upload_id.to_s.empty?
end
end
protected
def validate_parts!(parts)
validate!("parts", parts) do
if !parts.kind_of?(Array)
"must not be blank"
elsif parts.empty?
"must contain at least one entry"
elsif !parts.all? { |p| p.kind_of?(Hash) }
"must be an array of hashes"
elsif !parts.all? { |p| p[:part_number] }
"must contain part_number for each part"
elsif !parts.all? { |p| p[:etag] }
"must contain etag for each part"
elsif parts.any? { |p| p[:part_number].to_i < 1 }
"must not have part numbers less than 1"
end
end
end
protected
def json_validation_message(obj)
if obj.respond_to?(:to_str)
obj = obj.to_str
elsif obj.respond_to?(:to_json)
obj = obj.to_json
end
error = nil
begin
JSON.parse(obj)
rescue => e
error = e
end
"contains invalid JSON: #{error}" if error
end
protected
def xml_validation_message(obj)
if obj.respond_to?(:to_str)
obj = obj.to_str
elsif obj.respond_to?(:to_xml)
obj = obj.to_xml
end
error = nil
begin
REXML::Document.new(obj)
rescue => e
error = e
end
"contains invalid XML: #{error}" if error
end
end
include Validators
extend Validators
end
end
end
Jump to Line
Something went wrong with that request. Please try again.