class MSS::S3::PresignedPost

Helper to generate form fields for presigned POST requests to a bucket. You can use this to create a form that can be used from a web browser to upload objects to S3 while specifying conditions on what can be uploaded and how it is processed and stored.

@example Form fields for uploading by file name

form = bucket.presigned_post(:key => "photos/${filename}")
form.url.to_s        # => "https://mybucket.s3.amazonmss.com/"
form.fields          # => { "AWSAccessKeyId" => "...", ... }

@example Generating a minimal HTML form

form = bucket.objects.myobj.presigned_post
hidden_inputs = form.fields.map do |(name, value)|
  %(<input type="hidden" name="#{name}" value="#{value}" />)
end
<<-END
<form action="#{form.url}" method="post" enctype="multipart/form-data">
  #{hidden_inputs}
  <input type="file" name="file" />
</form>
END

@example Restricting the size of the uploaded object

bucket.presigned_post(:content_length => 1..(10*1024))

@example Restricting the key prefix

bucket.presigned_post.where(:key).starts_with("photos/")

Constants

SPECIAL_FIELDS

@api private

Attributes

bucket[R]

@return [Bucket] The bucket to which data can be uploaded

using the form fields
callback_body[R]
callback_body_type[R]
callback_host[R]
callback_url[R]
conditions[R]

@api private

content_length[R]

@return [Range] The range of acceptable object sizes for the

upload.  By default any size object may be uploaded.
expires[R]

@return The expiration time for the signature. By default

the signature will expire an hour after it is generated.
ignored_fields[R]

@return [Array<String>] Additional fields which may be sent

with the upload.  These will be included in the policy so
that they can be sent with any value.  S3 will ignore
them.
key[R]

@return [String] The key of the object that will be

uploaded.  If this is nil, then the object can be uploaded
with any key that satisfies the conditions specified for
the upload (see {#where}).
metadata[R]

@return [Hash] A hash of the metadata fields included in the

signed fields.  Additional metadata fields may be provided
with the upload as long as they satisfy the conditions
specified for the upload (see {#where}).

Public Class Methods

new(bucket, opts = {}) click to toggle source

Creates a new presigned post object.

@param [Bucket] bucket The bucket to which data can be uploaded

using the form fields.

@param [Hash] opts Additional options for the upload. Aside

from `:secure`, `:expires`, `:content_length` and `:ignore`
the values provided here will be stored in the hash returned
from the {#fields} method, and the policy in that hash will
restrict their values to the values provided.  If you
instead want to only restrict the values and not provide
them -- for example, if your application generates separate
form fields for those values -- you should use the {#where}
method on the returned object instead of providing the
values here.

@option opts [String] :key The key of the object that will

be uploaded.  If this is nil, then the object can be
uploaded with any key that satisfies the conditions
specified for the upload (see {#where}).

@option opts [Boolean] :secure By setting this to false, you

can cause {#url} to return an HTTP URL.  By default it
returns an HTTPS URL.

@option opts [Time, DateTime, Integer, String] :expires The

time at which the signature will expire.  By default the
signature will expire one hour after it is generated
(e.g. when {#fields} is called).

When the value is a Time or DateTime, the signature
expires at the specified time.  When it is an integer, the
signature expires the specified number of seconds after it
is generated.  When it is a string, the string is parsed
as a time (using Time.parse) and the signature expires at
that time.

@option opts [String] :cache_control Sets the Cache-Control

header stored with the object.

@option opts [String] :content_type Sets the Content-Type

header stored with the object.

@option opts [String] :content_disposition Sets the

Content-Disposition header stored with the object.

@option opts [String] :expires_header Sets the Expires

header stored with the object.

@option options [Symbol] :acl A canned access control

policy.  Valid values are:
* `:private`
* `:public_read`
* `:public_read_write`
* `:authenticated_read`
* `:bucket_owner_read`
* `:bucket_owner_full_control`

@option options [Symbol] :server_side_encryption (nil) If this

option is set, the object will be stored using server side
encryption.  The only valid value is `:aes256`, which
specifies that the object should be stored using the AES
encryption algorithm with 256 bit keys.  By default, this
option uses the value of the `:s3_server_side_encryption`
option in the current configuration; for more information,
see {MSS.config}.

@option opts [String] :success_action_redirect The URL to

which the client is redirected upon successful upload.

@option opts [Integer] :success_action_status The status

code returned to the client upon successful upload if
`:success_action_redirect` is not specified.  Accepts the
values 200, 201, or 204 (default).

If the value is set to 200 or 204, Amazon S3 returns an
empty document with a 200 or 204 status code.

If the value is set to 201, Amazon S3 returns an XML
document with a 201 status code.  For information on the
content of the XML document, see

@option opts [Hash] :metadata A hash of the metadata fields

included in the signed fields.  Additional metadata fields
may be provided with the upload as long as they satisfy
the conditions specified for the upload (see {#where}).

@option opts [Integer, Range] :content_length The range of

acceptable object sizes for the upload.  By default any
size object may be uploaded.

@option opts [Array<String>] :ignore Additional fields which

may be sent with the upload.  These will be included in
the policy so that they can be sent with any value.  S3
will ignore them.
Calls superclass method MSS::Core::Model::new
# File lib/mss/s3/presigned_post.rb, line 204
def initialize(bucket, opts = {})
  @bucket = bucket
  @key = opts[:key]
  @secure = (opts[:secure] != false)
  @fields = {}
  # TODO normalize all values to @fields
  opts.each do |opt_key, opt_val|
    @fields[opt_key] = opt_val unless SPECIAL_FIELDS.include? opt_key
  end
  @metadata = opts[:metadata] || {}
  @content_length = range_value(opts[:content_length])
  @conditions = opts[:conditions] || {}
  @ignored_fields = [opts[:ignore]].flatten.compact
  @expires = opts[:expires]
  @callback_url = opts[:callback_url]
  @callback_body = opts[:callback_body]
  @callback_body_type = opts[:callback_body_type]
  @callback_host = opts[:callback_host]

  super

  @fields[:server_side_encryption] =
    config.s3_server_side_encryption unless
    @fields.key?(:server_side_encryption)
  @fields.delete(:server_side_encryption) if
    @fields[:server_side_encryption].nil?
end

Public Instance Methods

fields() click to toggle source

@return [Hash] A collection of form fields (including a

signature and a policy) that can be used to POST data to
S3.  Additional form fields may be added after the fact as
long as they are described by a policy condition (see
{#where}).
# File lib/mss/s3/presigned_post.rb, line 359
def fields

  secret = config.credential_provider.secret_access_key
  signature = Core::Signers::Base.sign(secret, policy, 'sha1')

  fields = {
    "AWSAccessKeyId" => config.credential_provider.access_key_id,
    "key" => key,
    "bucket" => bucket.name,
    "policy" => policy,
    "signature" => signature
  }.merge(optional_fields)

  if token = config.credential_provider.session_token
    fields["x-amz-security-token"] = token
  end

  fields.merge(optional_fields)

end
policy() click to toggle source

@return [String] The Base64-encoded JSON policy document.

# File lib/mss/s3/presigned_post.rb, line 342
def policy
  json = {
    "expiration" => format_expiration,
    "conditions" => generate_conditions,
    "callbackUrl" => callback_url,
    "callbackBody" => callback_body,
    "callbackBodyType" => callback_body_type,
    "callbackHost" => callback_host
  }.to_json
  Base64.encode64(json).tr("\n","")
end
refine(opts) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 394
def refine(opts)
  self.class.new(bucket, {
                   :conditions => conditions,
                   :key => key,
                   :metadata => metadata,
                   :secure => secure?,
                   :content_length => content_length,
                   :expires => expires,
                   :ignore => ignored_fields
                 }.merge(@fields).
                 merge(opts))
end
secure?() click to toggle source

@return [Boolean] True if {#url} generates an HTTPS url.

# File lib/mss/s3/presigned_post.rb, line 233
def secure?
  @secure
end
url() click to toggle source

@return [URI::HTTP, URI::HTTPS] The URL to which the form

fields should be POSTed.  If you are using the fields in
an HTML form, this is the URL to put in the `action`
attribute of the form tag.
# File lib/mss/s3/presigned_post.rb, line 241
def url
  req = Request.new
  req.bucket = bucket.name
  req.host = config.s3_endpoint
  req.force_path_style = config.s3_force_path_style
  build_uri(req)
end
where(field) click to toggle source

Adds a condition to the policy for the POST. Use {#where_metadata} to add metadata conditions.

@example Restricting the ACL to “bucket-owner” ACLs

presigned_post.where(:acl).starts_with("bucket-owner")

@param [Symbol] field The field for which a condition should

be added. In addition to any arbitrary values you have set,
the following values are also permitted:

* `:key`
* `:content_length`
* `:cache_control`
* `:content_type`
* `:content_disposition`
* `:content_encoding`
* `:expires_header`
* `:acl`
* `:success_action_redirect`
* `:success_action_status`

@return [ConditionBuilder] An object that allows you to

specify a condition on the field.
# File lib/mss/s3/presigned_post.rb, line 321
def where(field)
  ConditionBuilder.new(self, field)
end
with_equality_condition(option_name, value) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 381
def with_equality_condition(option_name, value)
  field_name = field_name(option_name)
  with_condition(option_name, Hash[[[field_name, value]]])
end
with_prefix_condition(option_name, prefix) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 387
def with_prefix_condition(option_name, prefix)
  field_name = field_name(option_name)
  with_condition(option_name,
                 ["starts-with", "$#{field_name}", prefix])
end

Private Instance Methods

build_uri(request) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 564
def build_uri(request)
  uri_class = secure? ? URI::HTTPS : URI::HTTP
  uri_class.build(:host => request.host,
                  :path => request.path,
                  :query => request.querystring)
end
field_name(option_name) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 473
def field_name(option_name)
  case option_name
  when :expires_header
    "Expires"
  when :server_side_encryption
    "x-amz-server-side-encryption"
  when :key, "Key", :policy, "Policy", :bucket, "Bucket"
    option_name.to_s.downcase
  when :acl, :success_action_redirect, :success_action_status
    option_name.to_s
  else
    # e.g. Cache-Control from cache_control
    field_name = option_name.to_s.tr("_", "-").
      gsub(/-(.)/) { |m| m.upcase }
    field_name[0,1] = field_name[0,1].upcase
    field_name
  end
end
field_value(option_name) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 494
def field_value(option_name)
  case option_name
  when :acl
    @fields[:acl].to_s.tr("_", "-")
  when :server_side_encryption
    value = @fields[:server_side_encryption]
    if value.kind_of?(Symbol)
      value.to_s.upcase
    else
      value.to_s
    end
  else
    @fields[option_name].to_s
  end
end
format_expiration() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 417
def format_expiration
  time = expires || Time.now.utc + 60*60
  time =
    case time
    when Time
      time
    when DateTime
      Time.parse(time.to_s)
    when Integer
      (Time.now + time)
    when String
      Time.parse(time)
    end
  time.utc.iso8601
end
generate_conditions() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 512
def generate_conditions

  conditions = self.conditions.inject([]) do |list, (field, field_conds)|
    list + field_conds
  end

  conditions << { "bucket" => bucket.name }
  conditions += key_conditions
  conditions += optional_fields.map { |(n, v)| Hash[[[n, v]]] }
  conditions += range_conditions
  conditions += ignored_conditions

  if token = config.credential_provider.session_token
    conditions << { "x-amz-security-token" => token }
  end

  conditions

end
ignored_conditions() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 534
def ignored_conditions
  ignored_fields.map do |field|
    ["starts-with", "$#{field}", ""]
  end
end
key_conditions() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 552
def key_conditions
  [if key && key.include?("${filename}")
     ["starts-with", "$key", key[/^(.*)\$\{filename\}/, 1]]
   elsif key
     { "key" => key }
   else
     ["starts-with", "$key", ""]
   end]
end
optional_fields() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 456
      def optional_fields
        fields = @fields.keys.inject({}) do |fields, option_name|
          fields[field_name(option_name)] =
            field_value(option_name)
          fields
        end

        @metadata.each do |key, value|
#          fields["x-amz-meta-#{key}"] = value.to_s
          fields["#{key}"] = value.to_s
        end

        fields
      end
range_conditions() click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 542
def range_conditions
  if content_length
    [["content-length-range", *split_range(content_length)]]
  else
    []
  end
end
range_value(range) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 435
def range_value(range)
  case range
  when Integer
    range..range
  when Range
    range
  end
end
split_range(range) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 446
def split_range(range)
  range = range_value(range)
  [range.begin,
   (range.exclude_end? ?
    range.end-1 :
    range.end)]
end
with_condition(field, condition) click to toggle source

@api private

# File lib/mss/s3/presigned_post.rb, line 409
def with_condition(field, condition)
  conditions = self.conditions.dup
  (conditions[field] ||= []) << condition
  refine(:conditions => conditions)
end