Skip to content
Tristan Toye edited this page Oct 2, 2018 · 12 revisions

The server side is very straightforward. /sign takes a filename, type, and size then returns some JSON.

NOTE: if the client side uploader uploads multiple files, you'll have to sign each file separately.

Parameters

/sign?name=5854315625_2275398b48_o.jpg&size=1269959&type=image%2Fjpeg

or the more idiomatic:

GET /sign
{
  "name": "5854315625_2275398b48_o.jpg",
  "size": 1269959,
  "type": "image/jpeg"
}

Response

{
  "Expires": "Wed, 03 Oct 2018 09:24:24 GMT",
  "key": "5854315625_2275398b48_o.jpg",
  "success_action_status": "201",
  "Content-Disposition": "attachment",
  "Content-Type": "image/png",
  "Cache-Control": "max-age=630720000, public",
  "policy": "eyJleHBpcmF0aW9uIjoiMjAxOC0xMC0wM1QwMDoyNDoyNFoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJ0ZXN0LXVwbG9hZHMtY2FyZWVyanNtIn0seyJFeHBpcmVzIjoiV2VkLCAwMyBPY3QgMjAxOCAwOToyNDoyNCBHTVQifSx7ImtleSI6InN0YWdpbmcvdXNlci1yZWdpc3RyYXRpb25zLzMxMzUvZG9jdW1lbnRzL1NjcmVlbi1TaG90LTIwMTgtMTAtMDItYXQtMy41MS4xNS1QTS5wbmcifSx7InN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyI6IjIwMSJ9LHsiQ29udGVudC1EaXNwb3NpdGlvbiI6ImF0dGFjaG1lbnQifSx7IkNvbnRlbnQtVHlwZSI6ImltYWdlL3BuZyJ9LHsiQ2FjaGUtQ29udHJvbCI6Im1heC1hZ2U9NjMwNzIwMDAwLCBwdWJsaWMifSx7IngtYW16LWNyZWRlbnRpYWwiOiJBS0lBSlFNSllGRTZJSFYzTFVDUS8yMDE4MTAwMi9jYS1jZW50cmFsLTEvczMvYXdzNF9yZXF1ZXN0In0seyJ4LWFtei1hbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJ4LWFtei1kYXRlIjoiMjAxODEwMDJUMjMyNDI0WiJ9XX0=",
  "x-amz-credential": "AKIAJFFJYFE6IHV3LUCQ/87181002/ca-central-1/s3/aws4_request",
  "x-amz-algorithm": "AWS4-HMAC-SHA256",
  "x-amz-date": "20181002T232424Z",
  "x-amz-signature": "e6414e6dbda4988wwfa7ada51febcb777dq05eebf89ca3c0352233d2446fb",
  "bucket": "test-uploads"
}

These values tell the client-side where to post to, in order to make a successful request to S3.

Rails Setup

NOTE: If you are using Pundit library to handle authorization in your app, rename method policy() listed below to something else (e. g. s3_policy), because policy() is already defined in Pundit and it will cause authorization error (making policy() private doesn't help either).

def sign
  @expires = 10.hours.from_now.utc
  
  render json: signature, status: :ok
end

Signature uses the AWS Ruby SDK to generate a pre-signed post request. Follow instructions here for setup and config: https://github.com/aws/aws-sdk-ruby.

def signature
  client    = Aws::S3::Client.new
  resource  = Aws::S3::Resource.new(client: client)
  bucket    = resource.bucket ENV['AWS_BUCKET_NAME']

  presigned_post = bucket.presigned_post(
    acl: 'public-read',
    expires: @expires,
    key: upload_path,
    policy: policy,
    success_action_status: '201',
    content_disposition: 'attachment', # indicate this should be downloaded no opened in the browser
    content_type: params[:type],
    cache_control: 'max-age=630720000, public'
  )

  fields = presigned_post.fields
  fields['bucket'] = ENV['AWS_BUCKET_NAME']
  fields
end

Policy is base64-encoded JSON that is used by S3 to validate that the file is the same as the one attached.

def policy(options = {})
  Base64.strict_encode64(
    {
      expiration: @expires,
      conditions: [
        { bucket: 'sandbox' },
        { acl: 'public-read' },
        { expires: @expires },
        { success_action_status: '201' },
        [ 'starts-with', '$key', '' ],
        [ 'starts-with', '$Content-Type', '' ],
        [ 'starts-with', '$Cache-Control', '' ],
        [ 'content-length-range', 0, 524288000 ]
      ]
    }.to_json
  )
end
Clone this wiki locally