# Copyright 2011-2013 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' require 'base64' require 'nokogiri' module AWS class S3 # Client class for Amazon Simple Storage Service (S3). class Client < Core::Client signature_version :S3 API_VERSION = '2006-03-01' XMLNS = "http://s3.amazonaws.com/doc/#{API_VERSION}/" autoload :XML, 'aws/s3/client/xml' # @api private EMPTY_BODY_ERRORS = { 304 => Errors::NotModified, 403 => Errors::Forbidden, 400 => Errors::BadRequest, 404 => Errors::NoSuchKey, } # @api private CACHEABLE_REQUESTS = Set[] include DataOptions include Core::UriEscape # @param [Core::Http::Request] request # @api private def sign_request request version = @config.s3_signature_version ? @config.s3_signature_version.to_sym : (@region =~ /cn-/ ? :v4 : :v3) case version when :v4 then v4_signer.sign_request(request) when :v3 then v3_signer.sign_request(request) else raise "invalid signature version #{version.inspect}" end end protected # @return [Core::Signers::S3] def v3_signer @v3_signer ||= Core::Signers::S3.new(credential_provider) end # @return [Core::Signers::Version4] def v4_signer @v4_signer ||= begin Core::Signers::Version4.new(credential_provider, 's3', @region) end end # @param [Http::Request] req # @return [Boolean] def chunk_sign? req req.http_method == 'PUT' && req.headers['content-length'].to_i > 2 * 1024 * 1024 # 2MB end 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?(:rules)) verb = verb.to_s.upcase subresource = args.first add_client_request_method(method_name) 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 |(opt, header)| if value = options[opt] # for backwards compatability we translate canned acls # header values from symbols to strings (e.g. # :public_read translates to 'public-read') value = (opt == :acl ? value.to_s.tr('_', '-') : value) req.headers[header] = value end end end end instance_eval(&block) if block if xml_grammar parser = Core::XML::Parser.new(xml_grammar.rules) process_response do |resp| resp.data = parser.parse(resp.http_response.body) super(resp) end simulate_response do |resp| resp.data = parser.simulate super(resp) end end end end protected def set_metadata request, options if metadata = options[:metadata] Array(metadata).each do |name, value| request.headers["x-amz-meta-#{name}"] = value end end end def set_storage_class request, options storage_class = options[:storage_class] if storage_class.kind_of?(Symbol) request.headers["x-amz-storage-class"] = storage_class.to_s.upcase elsif storage_class request.headers["x-amz-storage-class"] = storage_class end end def set_server_side_encryption request, options sse = options[:server_side_encryption] if sse.is_a?(Symbol) request.headers['x-amz-server-side-encryption'] = sse.to_s.upcase elsif sse request.headers['x-amz-server-side-encryption'] = sse end end def extract_error_details response if (response.http_response.status >= 300 || response.request_type == :complete_multipart_upload) and body = response.http_response.body and error = Core::XML::Parser.parse(body) and error[:code] then [error[:code], error[:message]] end end def empty_response_body? response_body response_body.nil? or response_body == '' end # There are a few of s3 requests that can generate empty bodies and # yet still be errors. These return empty bodies to comply with the # HTTP spec. We have to detect these errors specially. def populate_error resp code = resp.http_response.status if EMPTY_BODY_ERRORS.include?(code) and empty_response_body?(resp.http_response.body) error_class = EMPTY_BODY_ERRORS[code] resp.error = error_class.new(resp.http_request, resp.http_response) else super end end def retryable_error? response super or failed_multipart_upload?(response) or response.error.is_a?(Errors::RequestTimeout) end # S3 may return a 200 response code in response to complete_multipart_upload # and then start streaming whitespace until it knows the final result. # At that time it sends an XML message with success or failure. def failed_multipart_upload? response response.request_type == :complete_multipart_upload && extract_error_details(response) end def new_request req = S3::Request.new req.force_path_style = config.s3_force_path_style? req end # Previously the access control policy could be specified via :acl # as a string or an object that responds to #to_xml. The prefered # method now is to pass :access_control_policy an xml document. def move_access_control_policy options if acl = options[:acl] if acl.is_a?(String) and is_xml?(acl) options[:access_control_policy] = options.delete(:acl) elsif acl.respond_to?(:to_xml) options[:access_control_policy] = options.delete(:acl).to_xml end end end # @param [String] possible_xml # @return [Boolean] Returns `true` if the given string is a valid xml # document. def is_xml? possible_xml begin REXML::Document.new(possible_xml).has_elements? rescue false end end def md5 str Base64.encode64(Digest::MD5.digest(str)).strip end def parse_copy_part_response resp doc = REXML::Document.new(resp.http_response.body) resp[:etag] = doc.root.elements["ETag"].text resp[:last_modified] = doc.root.elements["LastModified"].text if header = resp.http_response.headers['x-amzn-requestid'] data[:request_id] = [header].flatten.first end end def extract_object_headers resp meta = {} resp.http_response.headers.each_pair do |name,value| if name =~ /^x-amz-meta-(.+)$/i meta[$1] = [value].flatten.join end end resp.data[:meta] = meta if expiry = resp.http_response.headers['x-amz-expiration'] expiry.first =~ /^expiry-date="(.+)", rule-id="(.+)"$/ exp_date = DateTime.parse($1) exp_rule_id = $2 else exp_date = nil exp_rule_id = nil end resp.data[:expiration_date] = exp_date if exp_date resp.data[:expiration_rule_id] = exp_rule_id if exp_rule_id restoring = false restore_date = nil if restore = resp.http_response.headers['x-amz-restore'] if restore.first =~ /ongoing-request="(.+?)", expiry-date="(.+?)"/ restoring = $1 == "true" restore_date = $2 && DateTime.parse($2) elsif restore.first =~ /ongoing-request="(.+?)"/ restoring = $1 == "true" end end resp.data[:restore_in_progress] = restoring resp.data[:restore_expiration_date] = restore_date if restore_date { 'x-amz-version-id' => :version_id, 'content-type' => :content_type, 'content-encoding' => :content_encoding, 'cache-control' => :cache_control, 'expires' => :expires, 'etag' => :etag, 'x-amz-website-redirect-location' => :website_redirect_location, 'accept-ranges' => :accept_ranges, }.each_pair do |header,method| if value = resp.http_response.header(header) resp.data[method] = value end end if time = resp.http_response.header('Last-Modified') resp.data[:last_modified] = Time.parse(time) end if length = resp.http_response.header('content-length') resp.data[:content_length] = length.to_i end if sse = resp.http_response.header('x-amz-server-side-encryption') resp.data[:server_side_encryption] = sse.downcase.to_sym end end module Validators # @return [Boolean] 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 be between 3 and 63 characters long bucket_name.size > 63 or # Bucket names must only contain lowercase letters, numbers, dots, and dashes # and must start and end with a lowercase letter or a number bucket_name !~ /^[a-z0-9][a-z0-9.-]+[a-z0-9]$/ or # Bucket names should not be formatted like an IP address (e.g., 192.168.5.4) bucket_name =~ /(\d+\.){3}\d+/ 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 def validate! name, value, &block if error_msg = yield raise ArgumentError, "#{name} #{error_msg}" end value end def validate_key!(key) validate!('key', key) do case when key.nil? || key == '' 'may not be blank' end end end 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. 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-Za-z0-9._\-]+$/ 'may only contain uppercase letters, lowercase letters, numbers, periods (.), ' + 'underscores (_), and dashes (-)' when !(3..255).include?(bucket_name.size) 'must be between 3 and 255 characters long' when bucket_name =~ /\n/ 'must not contain a newline character' end end end 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 def require_acl! options acl_options = [ :acl, :grant_read, :grant_write, :grant_read_acp, :grant_write_acp, :grant_full_control, :access_control_policy, ] unless options.keys.any?{|opt| acl_options.include?(opt) } msg = "missing a required ACL option, must provide an ACL " + "via :acl, :grant_* or :access_control_policy" raise ArgumentError, msg end end def set_body_stream_and_content_length request, options unless options[:content_length] msg = "S3 requires a content-length header, unable to determine " msg << "the content length of the data provided, please set " msg << ":content_length" raise ArgumentError, msg end request.headers['content-length'] = options[:content_length] request.body_stream = options[:data] end def require_upload_id!(upload_id) validate!("upload_id", upload_id) do "must not be blank" if upload_id.to_s.empty? end end def require_part_number! part_number validate!("part_number", part_number) do "must not be blank" if part_number.to_s.empty? end end 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 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 def require_allowed_methods!(allowed_methods) validate!("allowed_methods", allowed_methods) do if !allowed_methods.kind_of?(Array) "must be an array" elsif !allowed_methods.all? { |x| x.kind_of?(String) } "must be an array of strings" end end end def require_allowed_origins!(allowed_origins) validate!("allowed_origins", allowed_origins) do if !allowed_origins.kind_of?(Array) "must be an array" elsif !allowed_origins.all? { |x| x.kind_of?(String) } "must be an array of strings" end end end end include Validators extend Validators end class Client::V20060301 < Client 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 # Creates a bucket. # @overload create_bucket(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] bucket_method(:create_bucket, :put, :header_options => { :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', }) do configure_request do |req, options| validate_bucket_name!(options[:bucket_name]) if location = options[:location_constraint] xmlns = "http://s3.amazonaws.com/doc/#{API_VERSION}/" req.body = <<-XML #{location} XML end super(req, options) end end alias_method :put_bucket, :create_bucket # @!method put_bucket_website(options = {}) # @param [Hash] options # @option (see WebsiteConfiguration#initialize) # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:put_bucket_website, :put, 'website') do configure_request do |req, options| req.body = Nokogiri::XML::Builder.new do |xml| xml.WebsiteConfiguration(:xmlns => XMLNS) do if redirect = options[:redirect_all_requests_to] xml.RedirectAllRequestsTo do xml.HostName(redirect[:host_name]) xml.Protocol(redirect[:protocol]) if redirect[:protocol] end end if indx = options[:index_document] xml.IndexDocument do xml.Suffix(indx[:suffix]) end end if err = options[:error_document] xml.ErrorDocument do xml.Key(err[:key]) end end rules = options[:routing_rules] if rules.is_a?(Array) && !rules.empty? xml.RoutingRules do rules.each do |rule| xml.RoutingRule do redirect = rule[:redirect] xml.Redirect do xml.Protocol(redirect[:protocol]) if redirect[:protocol] xml.HostName(redirect[:host_name]) if redirect[:host_name] xml.ReplaceKeyPrefixWith(redirect[:replace_key_prefix_with]) if redirect[:replace_key_prefix_with] xml.ReplaceKeyWith(redirect[:replace_key_with]) if redirect[:replace_key_with] xml.HttpRedirectCode(redirect[:http_redirect_code]) if redirect[:http_redirect_code] end if condition = rule[:condition] xml.Condition do xml.KeyPrefixEquals(condition[:key_prefix_equals]) if condition[:key_prefix_equals] xml.HttpErrorCodeReturnedEquals(condition[:http_error_code_returned_equals]) if condition[:http_error_code_returned_equals] end end end end end end end end.doc.root.to_xml super(req, options) end end # @overload get_bucket_website(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] # * `:index_document` - (Hash) # * `:suffix` - (String) # * `:error_document` - (Hash) # * `:key` - (String) bucket_method(:get_bucket_website, :get, 'website', XML::GetBucketWebsite) # @overload delete_bucket_website(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket_website, :delete, 'website') # Deletes an empty bucket. # @overload delete_bucket(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket, :delete) # @overload set_bucket_lifecycle_configuration(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :lifecycle_configuration # @return [Core::Response] bucket_method(:set_bucket_lifecycle_configuration, :put) do configure_request do |req, options| xml = options[:lifecycle_configuration] req.add_param('lifecycle') req.body = xml req.headers['content-md5'] = md5(xml) super(req, options) end end # @overload get_bucket_lifecycle_configuration(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] 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 resp.data = XML::GetBucketLifecycleConfiguration.parse(xml) end end # @overload delete_bucket_lifecycle_configuration(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket_lifecycle_configuration, :delete) do configure_request do |req, options| req.add_param('lifecycle') super(req, options) end end # @overload put_bucket_cors(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,Array] :rules An array of rule hashes. # * `:id` - (String) A unique identifier for the rule. The ID # value can be up to 255 characters long. The IDs help you find # a rule in the configuration. # * `:allowed_methods` - (required,Array) A list of HTTP # methods that you want to allow the origin to execute. # Each rule must identify at least one method. # * `:allowed_origins` - (required,Array) A list of origins # you want to allow cross-domain requests from. This can # contain at most one * wild character. # * `:allowed_headers` - (Array) A list of headers allowed # in a pre-flight OPTIONS request via the # Access-Control-Request-Headers header. Each header name # specified in the Access-Control-Request-Headers header must # have a corresponding entry in the rule. # Amazon S3 will send only the allowed headers in a response # that were requested. This can contain at most one * wild # character. # * `:max_age_seconds` - (Integer) The time in seconds that your # browser is to cache the preflight response for the specified # resource. # * `:expose_headers` - (Array) One or more headers in # the response that you want customers to be able to access # from their applications (for example, from a JavaScript # XMLHttpRequest object). # @return [Core::Response] bucket_method(:put_bucket_cors, :put) do configure_request do |req, options| req.add_param('cors') options[:rules].each do |rule| require_allowed_methods!(rule[:allowed_methods]) require_allowed_origins!(rule[:allowed_origins]) end xml = Nokogiri::XML::Builder.new do |xml| xml.CORSConfiguration do options[:rules].each do |rule| xml.CORSRule do xml.ID(rule[:id]) if rule[:id] (rule[:allowed_methods] || []).each do |method| xml.AllowedMethod(method) end (rule[:allowed_origins] || []).each do |origin| xml.AllowedOrigin(origin) end (rule[:allowed_headers] || []).each do |header| xml.AllowedHeader(header) end xml.MaxAgeSeconds(rule[:max_age_seconds]) if rule[:max_age_seconds] (rule[:expose_headers] || []).each do |header| xml.ExposeHeader(header) end end end end end.doc.root.to_xml req.body = xml req.headers['content-md5'] = md5(xml) super(req, options) end end # @overload get_bucket_cors(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_cors, :get) do configure_request do |req, options| req.add_param('cors') super(req, options) end process_response do |resp| resp.data = XML::GetBucketCors.parse(resp.http_response.body) end end # @overload delete_bucket_cors(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket_cors, :delete) do configure_request do |req, options| req.add_param('cors') super(req, options) end end # @overload put_bucket_tagging(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [Hash] :tags # @return [Core::Response] bucket_method(:put_bucket_tagging, :put) do configure_request do |req, options| req.add_param('tagging') xml = Nokogiri::XML::Builder.new xml.Tagging do |xml| xml.TagSet do options[:tags].each_pair do |key,value| xml.Tag do xml.Key(key) xml.Value(value) end end end end xml = xml.doc.root.to_xml req.body = xml req.headers['content-md5'] = md5(xml) super(req, options) end end # @overload get_bucket_tagging(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_tagging, :get) do configure_request do |req, options| req.add_param('tagging') super(req, options) end process_response do |resp| resp.data = XML::GetBucketTagging.parse(resp.http_response.body) end end # @overload delete_bucket_tagging(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket_tagging, :delete) do configure_request do |req, options| req.add_param('tagging') super(req, options) end end # @overload list_buckets(options = {}) # @param [Hash] options # @return [Core::Response] add_client_request_method(:list_buckets) do configure_request do |req, options| req.http_method = "GET" end process_response do |resp| resp.data = XML::ListBuckets.parse(resp.http_response.body) end simulate_response do |resp| resp.data = Core::XML::Parser.new(XML::ListBuckets.rules).simulate end end # Sets the access policy for a bucket. # @overload set_bucket_policy(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :policy This can be a String # or any object that responds to `#to_json`. # @return [Core::Response] 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. # @overload get_bucket_policy(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_policy, :get, 'policy') do process_response do |resp| resp.data[:policy] = resp.http_response.body end end # Deletes the access policy for a bucket. # @overload delete_bucket_policy(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:delete_bucket_policy, :delete, 'policy') # @overload set_bucket_versioning(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :state # @option options [String] :mfa_delete # @option options [String] :mfa # @return [Core::Response] bucket_method(:set_bucket_versioning, :put, 'versioning', :header_options => { :mfa => "x-amz-mfa" }) do configure_request do |req, options| state = options[:state].to_s.downcase.capitalize unless state =~ /^(Enabled|Suspended)$/ raise ArgumentError, "invalid versioning state `#{state}`" end # Leave validation of MFA options to S3 mfa_delete = options[:mfa_delete].to_s.downcase.capitalize if options[:mfa_delete] # Generate XML request for versioning req.body = Nokogiri::XML::Builder.new do |xml| xml.VersioningConfiguration('xmlns' => XMLNS) do xml.Status(state) xml.MfaDelete(mfa_delete) if mfa_delete end end.doc.root.to_xml super(req, options) end end # Gets the bucket's location constraint. # @overload get_bucket_location(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_location, :get, 'location') do process_response do |response| regex = />(.*)<\/LocationConstraint>/ matches = response.http_response.body.to_s.match(regex) response.data[:location_constraint] = matches ? matches[1] : nil end end # @overload put_bucket_logging(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [Boolean] :logging_enabled Set to true if turning on # bucket logging. If not set or false, all of the following options # will be ignored. # @option options [String] :target_bucket The name of the bucket in # which you want Amazon S3 to store server access logs. You can push # logs to any bucket you own, including the bucket being logged. # @option options [String] :target_prefix Allows you to specify a prefix # for the keys that the log files will be stored under. Recommended # if you will be writing logs from multiple buckets to the same target # bucket. # @option options [Array] :grants An array of hashes specifying # permission grantees. For each hash, specify ONLY ONE of :id, :uri, # or :email_address. # * `:email_address` - (String) E-mail address of the person being # granted logging permissions. # * `:id` - (String) The canonical user ID of the grantee. # * `:uri` - (String) URI of the grantee group. # * `:permission` - (String) Logging permissions given to the Grantee # for the bucket. The bucket owner is automatically granted FULL_CONTROL # to all logs delivered to the bucket. This optional element enables # you grant access to others. Valid Values: FULL_CONTROL | READ | WRITE # @return [Core::Response] bucket_method(:put_bucket_logging, :put) do configure_request do |req, options| req.add_param('logging') xml = Nokogiri::XML::Builder.new xml.BucketLoggingStatus('xmlns' => XMLNS) do |xml| if options[:logging_enabled] == true xml.LoggingEnabled do xml.TargetBucket(options[:target_bucket]) xml.TargetPrefix(options[:target_prefix]) unless options[:grants].nil? xml.TargetGrants do options[:grants].each do |grant| xml.Grant do if !grant[:email_address].nil? xml.Grantee('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'AmazonCustomerByEmail') do xml.EmailAddress(grant[:email_address]) end elsif !grant[:uri].nil? xml.Grantee('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'Group') do xml.URI(grant[:uri]) end elsif !grant[:id].nil? xml.Grantee('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'CanonicalUser') do xml.ID(grant[:id]) end end xml.Permission(grant[:permission]) end end end end end end end xml = xml.doc.root.to_xml req.body = xml req.headers['content-md5'] = md5(xml) super(req, options) end end # Gets the bucket's logging status. # @overload get_bucket_logging(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_logging, :get, 'logging', XML::GetBucketLogging) # @overload get_bucket_versioning(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_versioning, :get, 'versioning', XML::GetBucketVersioning) # @overload list_object_versions(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [String] :prefix # @option options [String] :delimiter # @option options [String] :max_keys # @option options [String] :key_marker # @option options [String] :version_id_marker # @return [Core::Response] 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. You must specify an ACL # via one of the following methods: # # * as a canned ACL (via `:acl`) # * as a list of grants (via the `:grant_*` options) # * as an access control policy document (via `:access_control_policy`) # # @example Using a canned acl # s3_client.put_bucket_acl( # :bucket_name => 'bucket-name', # :acl => 'public-read') # # @example Using grants # s3_client.put_bucket_acl( # :bucket_name => 'bucket-name', # :grant_read => 'uri="http://acs.amazonaws.com/groups/global/AllUsers"', # :grant_full_control => 'emailAddress="xyz@amazon.com", id="8a9...fa7"') # # @example Using an access control policy document # policy_xml = <<-XML # # # 852b113e7a2f25102679df27bb0ae12b3f85be6BucketOwnerCanonicalUserID # OwnerDisplayName # # # # # BucketOwnerCanonicalUserID # OwnerDisplayName # # FULL_CONTROL # # # # http://acs.amazonaws.com/groups/global/AllUsers # # READ # # # # # XML # s3_client.put_bucket_acl( # :bucket_name => 'bucket-name', # :access_control_policy => policy_xml) # # @overload put_bucket_acl(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [String] :access_control_policy An access control # policy description as a string of XML. See the S3 API # documentation for a description. # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] bucket_method(:put_bucket_acl, :put, 'acl', :header_options => { :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', }) do configure_request do |req, options| move_access_control_policy(options) require_acl!(options) super(req, options) req.body = options[:access_control_policy] if options[:access_control_policy] end end alias_method :set_bucket_acl, :put_bucket_acl # Gets the access control list for a bucket. # @overload get_bucket_acl(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @return [Core::Response] bucket_method(:get_bucket_acl, :get, 'acl', XML::GetBucketAcl) # Sets the access control list for an object. You must specify an ACL # via one of the following methods: # # * as a canned ACL (via `:acl`) # * as a list of grants (via the `:grant_*` options) # * as an access control policy document (via `:access_control_policy`) # # @example Using a canned acl # s3_client.put_object_acl( # :bucket_name => 'bucket-name', # :key => 'object-key', # :acl => 'public-read') # # @example Using grants # s3_client.put_bucket_acl( # :bucket_name => 'bucket-name', # :key => 'object-key', # :grant_read => 'uri="http://acs.amazonaws.com/groups/global/AllUsers"', # :grant_full_control => 'emailAddress="xyz@amazon.com", id="8a9...fa7"') # # @example Using an access control policy document # policy_xml = <<-XML # # # 852b113e7a2f25102679df27bb0ae12b3f85be6BucketOwnerCanonicalUserID # OwnerDisplayName # # # # # BucketOwnerCanonicalUserID # OwnerDisplayName # # FULL_CONTROL # # # # http://acs.amazonaws.com/groups/global/AllUsers # # READ # # # # # XML # s3_client.put_bucket_acl( # :bucket_name => 'bucket-name', # :key => 'object-key', # :access_control_policy => policy_xml) # # @overload put_object_acl(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [String] :access_control_policy An access control # policy description as a string of XML. See the S3 API # documentation for a description. # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] object_method(:put_object_acl, :put, 'acl', :header_options => { :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', }) do configure_request do |req, options| move_access_control_policy(options) require_acl!(options) super(req, options) req.body = options[:access_control_policy] if options[:access_control_policy] end end alias_method :set_object_acl, :put_object_acl # Gets the access control list for an object. # @overload get_object_acl(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @return [Core::Response] object_method(:get_object_acl, :get, 'acl', XML::GetBucketAcl) # Puts data into an object, replacing the current contents. # # s3_client.put_object({ # :bucket_name => 'bucket-name', # :key => 'readme.txt', # :data => 'This is the readme for ...', # }) # # @overload put_object(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,String,Pathname,File,IO] :data # The data to upload. This can be provided as a string, # a Pathname object, or any object that responds to # `#read` and `#eof?` (e.g. IO, File, Tempfile, StringIO, etc). # @option options [Integer] :content_length # Required if you are using block form to write data or if it is # not possible to determine the size of `:data`. A best effort # is made to determine the content length of strings, files, # tempfiles, io objects, and any object that responds # to `#length` or `#size`. # @option options [String] :website_redirect_location If the bucket is # configured as a website, redirects requests for this object to # another object in the same bucket or to an external URL. # @option options [Hash] :metadata # A hash of metadata to be included with the # object. These will be sent to S3 as headers prefixed with # `x-amz-meta`. # @option options [Symbol] :acl (:private) A canned access # control policy. Accepted values include: # * `:private` # * `:public_read` # * ... # @option options [String] :storage_class+ ('STANDARD') # Controls whether Reduced Redundancy Storage is enabled for # the object. Valid values are 'STANDARD' and # 'REDUCED_REDUNDANCY'. # @option options [Symbol,String] :server_side_encryption (nil) The # algorithm used to encrypt the object on the server side # (e.g. :aes256). # object on the server side, e.g. `:aes256`) # @option options [String] :cache_control # Can be used to specify caching behavior. # @option options [String] :content_disposition # Specifies presentational information. # @option options [String] :content_encoding # Specifies the content encoding. # @option options [String] :content_md5 # The base64 encoded content md5 of the `:data`. # @option options [String] :content_type # Specifies the content type. # @option options [String] :expires The date and time at which the # object is no longer cacheable. # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] # object_method(:put_object, :put, :header_options => { :website_redirect_location => 'x-amz-website-redirect-location', :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', :content_md5 => 'Content-MD5', :cache_control => 'Cache-Control', :content_disposition => 'Content-Disposition', :content_encoding => 'Content-Encoding', :content_type => 'Content-Type', :expires => 'Expires', }) do configure_request do |request, options| options = compute_write_options(options) set_body_stream_and_content_length(request, options) set_metadata(request, options) set_storage_class(request, options) set_server_side_encryption(request, options) super(request, options) end process_response do |resp| extract_object_headers(resp) end simulate_response do |response| response.data[:etag] = 'abc123' response.data[:version_id] = nil end end # Gets the data for a key. # @overload get_object(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [Time] :if_modified_since If specified, the # response will contain an additional `:modified` value that # returns true if the object was modified after the given # time. If `:modified` is false, then the response # `:data` value will be `nil`. # @option options [Time] :if_unmodified_since If specified, the # response will contain an additional `:unmodified` value # that is true if the object was not modified after the # given time. If `:unmodified` returns false, the `:data` # value will be `nil`. # @option options [String] :if_match If specified, the response # will contain an additional `:matches` value that is true # if the object ETag matches the value for this option. If # `:matches` is false, the `:data` value of the # response will be `nil`. # @option options [String] :if_none_match If specified, the # response will contain an additional `:matches` value that # is true if and only if the object ETag matches the value for # this option. If `:matches` is true, the `:data` value # of the response will be `nil`. # @option options [Range] :range A byte range of data to request. # @return [Core::Response] # 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] if range.is_a?(Range) offset = range.exclude_end? ? -1 : 0 range = "bytes=#{range.first}-#{range.last + offset}" end req.headers['Range'] = range end end process_response do |resp| extract_object_headers(resp) resp.data[:data] = resp.http_response.body end end # Gets the torrent for a key. # @overload get_object_torrent(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @return [Core::Response] # object_method(:get_object_torrent, :get, 'torrent') do process_response do |resp| extract_object_headers(resp) resp.data[:data] = resp.http_response.body end end # @overload head_object(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [String] :version_id # @return [Core::Response] 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| extract_object_headers(resp) end end # @overload delete_object(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [String] :version_id # @option options [String] :mfa # @return [Core::Response] object_method(:delete_object, :delete, :header_options => { :mfa => "x-amz-mfa" }) 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| resp.data[:version_id] = resp.http_response.header('x-amz-version-id') end end # @overload restore_object(options = {}) # Restores a temporary copy of an archived object. # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,Integer] :days the number of days to keep # the restored object. # @return [Core::Response] # @since 1.7.2 object_method(:restore_object, :post, 'restore', :header_options => { :content_md5 => 'Content-MD5' }) do configure_request do |req, options| super(req, options) validate!(:days, options[:days]) do "must be greater or equal to 1" if options[:days].to_i <= 0 end xml = Nokogiri::XML::Builder.new do |xml| xml.RestoreRequest('xmlns' => XMLNS) do xml.Days(options[:days].to_i) if options[:days] end end.doc.root.to_xml req.body = xml req.headers['content-type'] = 'application/xml' req.headers['content-md5'] = md5(xml) end end # @overload list_objects(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [String] :delimiter # @option options [String] :marker # @option options [String] :max_keys # @option options [String] :prefix # @return [Core::Response] 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 alias_method :get_bucket, :list_objects # @overload initiate_multipart_upload(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [String] :website_redirect_location If the bucket is # configured as a website, redirects requests for this object to # another object in the same bucket or to an external URL. # @option options [Hash] :metadata # @option options [Symbol] :acl # @option options [String] :cache_control # @option options [String] :content_disposition # @option options [String] :content_encoding # @option options [String] :content_type # @option options [String] :storage_class+ ('STANDARD') # Controls whether Reduced Redundancy Storage is enabled for # the object. Valid values are 'STANDARD' and # 'REDUCED_REDUNDANCY'. # @option options [Symbol,String] :server_side_encryption (nil) The # algorithm used to encrypt the object on the server side # (e.g. :aes256). # @option options [String] :expires The date and time at which the # object is no longer cacheable. # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] object_method(:initiate_multipart_upload, :post, 'uploads', XML::InitiateMultipartUpload, :header_options => { :website_redirect_location => 'x-amz-website-redirect-location', :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', :cache_control => 'Cache-Control', :content_disposition => 'Content-Disposition', :content_encoding => 'Content-Encoding', :content_type => 'Content-Type', :expires => 'Expires', }) do configure_request do |req, options| set_metadata(req, options) set_storage_class(req, options) set_server_side_encryption(req, options) super(req, options) end process_response do |resp| extract_object_headers(resp) end end # @overload list_multipart_uploads(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [String] :delimiter # @option options [String] :key_marker # @option options [String] :max_keys # @option options [String] :upload_id_marker # @option options [String] :max_uploads # @option options [String] :prefix # @return [Core::Response] 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 # @overload delete_objects(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,Array] :objects Each entry should be # a hash with the following keys: # * `:key` - *required* # * `:version_id` # @option options [Boolean] :quiet (true) # @option options [String] :mfa # @return [Core::Response] bucket_method(:delete_objects, :post, 'delete', XML::DeleteObjects, :header_options => { :mfa => "x-amz-mfa" } ) do configure_request do |req, options| super(req, options) req.body = Nokogiri::XML::Builder.new do |xml| xml.Delete do xml.Quiet(options.key?(:quiet) ? options[:quiet] : true) (options[:objects] || options[:keys]).each do |obj| xml.Object do xml.Key(obj[:key]) xml.VersionId(obj[:version_id]) if obj[:version_id] end end end end.doc.root.to_xml req.headers['content-md5'] = md5(req.body) end end # @overload upload_part(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,String] :upload_id # @option options [required,Integer] :part_number # @option options [required,String,Pathname,File,IO] :data # The data to upload. This can be provided as a string, # a Pathname object, or any object that responds to # `#read` and `#eof?` (e.g. IO, File, Tempfile, StringIO, etc). # @return [Core::Response] object_method(:upload_part, :put, :header_options => { :content_md5 => 'Content-MD5' }) do configure_request do |request, options| options = compute_write_options(options) set_body_stream_and_content_length(request, options) require_upload_id!(options[:upload_id]) request.add_param('uploadId', options[:upload_id]) require_part_number!(options[:part_number]) request.add_param('partNumber', options[:part_number]) super(request, options) end process_response do |resp| extract_object_headers(resp) end simulate_response do |response| response.data[:etag] = 'abc123' end end # @overload complete_multipart_upload(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,String] :upload_id # @option options [required,Array] :parts An array of hashes # with the following keys: # * `:part_number` [Integer] - *required* # * `:etag` [String] - *required* # @return [Core::Response] 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]) req.body = Nokogiri::XML::Builder.new do |xml| xml.CompleteMultipartUpload do options[:parts].each do |part| xml.Part do xml.PartNumber(part[:part_number]) xml.ETag(part[:etag]) end end end end.doc.root.to_xml end process_response do |resp| extract_object_headers(resp) end simulate_response do |response| response.data = {} end end # @overload abort_multipart_upload(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,String] :upload_id # @return [Core::Response] 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 # @overload list_parts(options = {}) # @param [Hash] options # @option options [required,String] :bucket_name # @option options [required,String] :key # @option options [required,String] :upload_id # @option options [Integer] :max_parts # @option options [Integer] :part_number_marker # @return [Core::Response] 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 # Copies an object from one key to another. # @overload copy_object(options = {}) # @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 [String] :website_redirect_location If the bucket is # configured as a website, redirects requests for this object to # another object in the same bucket or to an external URL. # @option options [required, String] :copy_source The source # bucket name and key, joined by a forward slash ('/'). # This string must be URL-encoded. Additionally, you must # have read access to the source object. # @option options [String] :acl A canned ACL (e.g. 'private', # 'public-read', etc). See the S3 API documentation for # a complete list of valid values. # @option options [Symbol,String] :server_side_encryption (nil) The # algorithm used to encrypt the object on the server side # (e.g. :aes256). # @option options [String] :storage_class+ ('STANDARD') # Controls whether Reduced Redundancy Storage is enabled for # the object. Valid values are 'STANDARD' and # 'REDUCED_REDUNDANCY'. # @option options [String] :expires The date and time at which the # object is no longer cacheable. # @option options [String] :grant_read # @option options [String] :grant_write # @option options [String] :grant_read_acp # @option options [String] :grant_write_acp # @option options [String] :grant_full_control # @return [Core::Response] object_method(:copy_object, :put, :header_options => { :website_redirect_location => 'x-amz-website-redirect-location', :acl => 'x-amz-acl', :grant_read => 'x-amz-grant-read', :grant_write => 'x-amz-grant-write', :grant_read_acp => 'x-amz-grant-read-acp', :grant_write_acp => 'x-amz-grant-write-acp', :grant_full_control => 'x-amz-grant-full-control', :copy_source => 'x-amz-copy-source', :cache_control => 'Cache-Control', :metadata_directive => 'x-amz-metadata-directive', :content_type => 'Content-Type', :content_disposition => 'Content-Disposition', :expires => 'Expires', }) do configure_request do |req, options| 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])) super(req, options) set_metadata(req, options) set_storage_class(req, options) set_server_side_encryption(req, options) if options[:version_id] req.headers['x-amz-copy-source'] += "?versionId=#{options[:version_id]}" end end process_response do |resp| extract_object_headers(resp) end end object_method(:copy_part, :put, XML::CopyPart, :header_options => { :copy_source => 'x-amz-copy-source', :copy_source_range => 'x-amz-copy-source-range', }) do configure_request do |request, options| validate!(:copy_source, options[:copy_source]) do "may not be blank" if options[:copy_source].to_s.empty? end validate!(:copy_source_range, options[:copy_source_range]) do "must start with bytes=" if options[:copy_source_range] && !options[:copy_source_range].start_with?("bytes=") end options = options.merge(:copy_source => escape_path(options[:copy_source])) require_upload_id!(options[:upload_id]) request.add_param('uploadId', options[:upload_id]) require_part_number!(options[:part_number]) request.add_param('partNumber', options[:part_number]) super(request, options) if options[:version_id] req.headers['x-amz-copy-source'] += "?versionId=#{options[:version_id]}" end end end end end end