Search code examples
jqueryruby-on-railsamazon-s3content-typejquery-file-upload

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?


Solution

  • 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("")