openwonk
openwonk

Reputation: 15567

Javascript not working when uploading file to AWS S3 via presigned URL

I generate a pre-signed URL for uploading files to AWS S3 via a Python-backed API. This part receives filename info once the user selects the file in a browser (see below), and returns a JSON payload (see below) with the base URL and fields.

import logging
import boto3
from botocore.exceptions import ClientError


def create_presigned_post(bucket_name, object_name,
                          fields=None, conditions=None, expiration=3600):

    # Generate a presigned S3 POST URL
    s3_client = boto3.client('s3')
    try:
        response = s3_client.generate_presigned_post(bucket_name,
                                                     object_name,
                                                     Fields=fields,
                                                     Conditions=conditions,
                                                     ExpiresIn=expiration)
    except ClientError as e:
        logging.error(e)
        return None

    # The response contains the presigned URL and required fields
    return response

Here's the JSON response I get from this function. (I changed/abbreviated some of the values, but you get the point.)

{
  "url": "https://s3.us-east-2.amazonaws.com/my_bucket_name",
  "fields": {
    "key": "some.txt",
    "AWSAccessKeyId": "ASI...",
    "x-amz-security-token": "Ag9o...",
    "policy": "eyJ...=",
    "signature": "jn...="
  }
}

Here's the HTML form that I'm using to upload the file. I have some vanilla Javascript that tracks changes to the form and updates the URL_VALUE for the form and the VALUE for each form item upon changes (e.g. file selection).

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <!-- Copy the 'url' value returned by S3Client.generate_presigned_post() -->
    <form action="URL_VALUE" method="post" enctype="multipart/form-data">
      <!-- Copy the 'fields' key:values returned by S3Client.generate_presigned_post() -->
      <input type="hidden" name="key" value="VALUE" />
      <input type="hidden" name="AWSAccessKeyId" value="VALUE" />
      <input type="hidden" name="policy" value="VALUE" />
      <input type="hidden" name="signature" value="VALUE" />
    File:
      <input type="file"   name="file" /> <br />
      <input type="submit" name="submit" value="Upload to Amazon S3" />
    </form>
  </body>
</html>

This HTML form works fine by itself, but I tried adding some Javascript (both vanilla and JQuery), so I can track file progress and disable form inputs until the upload is complete.

I can't get Javascript to work!!!

I have tried so many examples (again, both vanilla JS and JQuery).

Has anyone recently implemented this and can help?

Upvotes: 2

Views: 2854

Answers (4)

T H
T H

Reputation: 558

I finally figure it out. This is the code.

    formData = new FormData()
    Object.keys(presignedPostData.fields).forEach(key => {
        formData.append(key, presignedPostData.fields[key])
    })

    file = $('input[type="file"]')[0].files[0]
    formData.append("file", file)

    fetch(presignedPostData.url, {
        method: 'POST',
        body: formData,
        mode: 'no-cors'
    }).then((response) => {
        lg('response', response)
    })

Upvotes: 0

Ishan Joshi
Ishan Joshi

Reputation: 525

I believe you have to pass the AWS secrets such as

"key": "some.txt",
    "AWSAccessKeyId": "ASI...",
    "x-amz-security-token": "Ag9o...",
    "policy": "eyJ...=",
    "signature": "jn...="

as headers.

Are you using the fetch library? Can you please post the JS code to?

Upvotes: 1

Brad
Brad

Reputation: 163311

It's even easier than what you've posted.

fetch(yourSignedUrl, {
  method: 'PUT',
  body: file,
  headers: {
    'Content-Type': file.type
  }
}).then((res) => {
  if (!res.ok) {
    throw new Error(res.statusText);
  }
  return res.headers.get('ETag');
});

Upvotes: 1

openwonk
openwonk

Reputation: 15567

Alright, found a ridiculously simple vanilla JS example that works!


$(document).ready(function () {
    var PRESIGNED_URL = ""

    $('input[type="file"]').change(function (e) {
        var fileName = e.target.files[0].name;

        var settings = {
            "async": true,
            "crossDomain": true,
            "url": "https://my_api.com",
            "method": "POST",
            "headers": {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            "data": {
                "filename": fileName
            }
        }

        $.ajax(settings).done(function (response) {
            $("#filename").html(fileName)
            PRESIGNED_URL = response["url"]

            $("#form").attr("action", response["url"])
            $("#key").val(response["fields"]["key"])
            $("#AWSAccessKeyId").val(response["fields"]["AWSAccessKeyId"])
            $("#policy").val(response["fields"]["policy"])
            $("#signature").val(response["fields"]["signature"])
            $("#x-amz-security-token").val(response["fields"]["x-amz-security-token"])

            return
        });
    });

    $("#button").on("click", function (e) {
        var form = document.getElementById('form');
        var formData = new FormData(form);

        var xhr = new XMLHttpRequest();
        // Add any event handlers here...

        xhr.upload.addEventListener('progress', function(e) {
            var percent_complete = (e.loaded / e.total)*100;

            // Percentage of upload completed
            console.log(percent_complete);
        });
        xhr.onloadstart = function (e) {
            console.log("start")
        }
        xhr.onloadend = function (e) {
            console.log("end")
        }

        xhr.open('POST', PRESIGNED_URL, true);
        xhr.send(formData);

        $("#filename").html("")
    })
});



There are so many close variations, but this on worked perfectly.

(I'm sure it seems obvious to many, but I only do front-end dev by necessity...)

Upvotes: -1

Related Questions