Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting the Content-Type in direct to S3 upload using Rails and jQuery File Upload

I've seen many questions here on this topic but nothing I've come across has worked for me, so here I am posting another...

I'm on Ruby on Rails trying to configure file uploads direct to Amazon S3 using the jQuery File Upload plugin. I followed along with the very helpful Heroku tutorial to get the initial setup working. Files uploaded fine, but they were all labeled as Content-Type: binary/octet-stream in S3, so when they were served in the app, all files would download instead of opening directly.

This is a problem because I'm trying to allow images, PDFs, audio or video files, so I need to be able to grab the correct Content-Type from the file and pass it on to S3. In looking at the AWS-SDK gem docs on Amazon, I saw this section about adding .where(:content_type).starts_with("") to the end of the presigned post object to modify the policy. However, when I did that, it threw an error:

<Error><Code>AccessDenied</Code>
<Message>Invalid according to Policy: Policy Condition failed: ["starts-with", "$Content-Type", ""]</Message>

So I added in content_type: "" into the opts hash for the presigned post object, and now it works again, but instead of all files defaulting to binary/octet-stream they all default to image/jpeg. Here's my code as of now:

Controller

def new
  @s3_direct_post = S3_BUCKET.presigned_post(
                  key: "uploads/#{SecureRandom.uuid}/${filename}",
                  success_action_status: 201,
                  acl: :public_read,
                  content_type: "").where(:content_type).starts_with("")
end

_form.html.haml

:javascript
  $(function() {
    $('.directUpload').find("input:file").each(function(i, elem) {
      var fileInput    = $(elem);
      var form         = $(fileInput.parents('form:first'));
      var submitButton = form.find('input[type="submit"]');
      var progressBar  = $("<div class='bar'></div>");
      var barContainer = $("<div class='progress'></div>").append(progressBar);
      var fd           = #{@s3_direct_post.fields.to_json.html_safe};
      fileInput.after(barContainer);
      fileInput.fileupload({
        // This 'add' section is where I thought to set the Content-Type, but I've tried  with and without it and Content-Type remains the same on S3
        add: function (e, data) {
          fd["Content-Type"] = data.files[0].type;  
          console.log(fd);    // The JSON object shows Content-Type correctly in console
          data.submit();
        },  
        fileInput:        fileInput,
        url:              '#{@s3_direct_post.url}',
        type:             'POST',
        autoUpload:       true,
        formData:         fd,   // My updated JSON object
        paramName:        'file',
        dataType:         'XML',
        replaceFileInput: false,
        progressall: function (e, data) {
          var progress = parseInt(data.loaded / data.total * 100, 10);
          progressBar.css('width', progress + '%')
        },
        start: function (e) {
          submitButton.prop('disabled', true);

          progressBar.
            css('background', 'green').
            css('display', 'block').
            css('width', '0%').
            text("Loading...");
        },
        done: function(e, data) {
          submitButton.prop('disabled', false);
          progressBar.text("Uploading done");

          // extract key and generate URL from response
          var key   = $(data.jqXHR.responseXML).find("Key").text();
          var url   = 'https://d295xbrl26r3ll.cloudfront.net/' + key.replace(/ /g, "%20");

          // create hidden field
          var input = $("<input />", { type:'hidden', name: 'item[file_url]', value: url })
          form.append(input);
        },
        fail: function(e, data) {
          submitButton.prop('disabled', false);

          progressBar.
            css("background", "red").
            text("Failed");
        }
      });
    });
  });

How do I send the Content-Type properly to S3?

like image 411
supremebeing7 Avatar asked Aug 14 '14 18:08

supremebeing7


3 Answers

If anyone is looking for an updated answer to this question, I was able to solve this by:

  1. Adding a 'content type starts with' clause to my s3 presigned post, as documented on this page: http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
  2. Adding a hidden input named 'Content-Type' to my form and then updating it accordingly whenever a file is selected.
like image 106
Ken Hill Avatar answered Oct 31 '22 16:10

Ken Hill


Replace your add block with:

      fd["Content-Type"] = data.files[0].type;  
      data.formData = fd;
      data.submit();

The callback is correct, but data.formData already took the original version of fd. Just set it again with your modified fd and you should be good to go.

Update the controller method too so you're not doing it twice:

@s3_direct_post = S3_BUCKET.presigned_post(
                  key: "uploads/#{SecureRandom.uuid}/${filename}",
                  success_action_status: 201,
                  acl: :public_read).where(:content_type).starts_with("")
like image 15
Will Tungpagasit Avatar answered Oct 31 '22 16:10

Will Tungpagasit


I believe the most current syntax for aws-sdk (2.9.6) is:

@s3_direct_post = S3_BUCKET.presigned_post(
                  key: "uploads/#{SecureRandom.uuid}/${filename}",
                  success_action_status: 201,
                  acl: 'public-read',
                  content_type_starts_with: '')
like image 2
dominik Avatar answered Oct 31 '22 15:10

dominik