Thursday, April 27, 2017

Uploading Images to Amazon S3 Directly from the Browser Using S3 Direct Uploads



In this series of posts, I am writing about various AWS services. In my previous posts, I have written about AWS EC2, Elastic Load Balancing, Auto Scaling, DynamoDB, Amazon Simple Queue Service and Amazon Simple Email Service.

In my last post, I have added an user activation functionality to my digital card store application. The application is used for managing digital cards. So far I have added, user registration, user session management, selling and buying cards and user activation functionality. The user can add a new card by specifying a name only.

In this post, I will add an upload function that allows user to attach an image to a digital card. I will use Amazon S3 to store uploaded image files.

Upload Functionality

When we think about uploading a file, the first option that comes to mind is to upload the file to an EC2 instance from the browser and then send the file to Amazon S3 from the EC2 instance.
While this method accomplish the image upload requirement, there is a better method. In 2012, Amazon announced CORS support for Amazon S3, which allows any web application to upload files to S3 directly. This allows quick and efficient uploads and eliminates proxying the upload requests.

The picture below shows the upload process.



To use direct uploads to S3, we should follow the steps below.

1. Enable CORS support for the bucket.
2. Configure access permissions.
3. Develop the signing part in server
4. Prepare the web front end.

In this post, I will start with the code from my last post. The code can be found here. In the post that I have written about DynamoDB, I have generated the Card entity class with imageURL field. In this post, I will use this field to hold the URL of the uploaded image file. There will be no change in entity class and CardController class. After the image uploaded to the S3, its url will be passed as imageURL to add card request. The card will be persisted with this url to DynamoDB and it will be used as card image url to show the card image in the card listing table. The final code for this post can be found here.

Let's start.

1. Enable CORS support for the bucket

To be able to use direct uploads from any web application, the target S3 bucket should be configured to allow requests from a different domain. For more information, see S3 Cors documentation.

To enable CORS support using Amazon Console, use the steps below. To use AWS CLI, see here.

  • ·         Login to Amazon Console and select S3
  • ·         Select your bucket and click Properties
  • ·         Click Permissions and then click Edit CORS Configuration
  • ·         Paste the below configuration and click Save and then click Close.


<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
       <CORSRule>
             <AllowedOrigin>*</AllowedOrigin>
             <AllowedMethod>GET</AllowedMethod>
             <AllowedMethod>POST</AllowedMethod>
             <AllowedMethod>PUT</AllowedMethod>
             <AllowedHeader>*</AllowedHeader>
       </CORSRule>
</CORSConfiguration>

Please note that I allowed any origin to post to the bucket for easy development. To provide robust securiy in production use, please restrict the domains accordingly.

2. Configure access permissions

To allow uploads to the S3 bucket, the bucket should be writable. There are a few options for this. The first option is to make the bucket public. But if you make the bucket public, everybody can write to your bucket and you take the risk of uncontrolled uploads. The second option is to make the bucket writable by a specific IAM user and sign the upload requests with this users credentials. This option is more secure compared to first option. In this post, I will use the second option. Please consider security options before using the upload function in production.

While using signed requests, S3 expects your upload requests to include an upload policy and a signature. The signature is prepared using your IAM credentials. You can use any IAM credentials, but to provide more strict security, please use a dedicated IAM user for this purpose that have write access only to the target S3 bucket. This way, you can be sure that in case of a credential disclosure, it affects only a specific S3 bucket, not any other AWS resources. For more strict security, you can use Temporary Security Credentials.

To configure access permissions using Amazon Console, use the steps below. To use AWS CLI, see here.

  • ·         Login to Amazon Console and select S3
  • ·         Select your bucket and click Properties
  • ·         Click Permissions and then click Edit bucket policy.
  • ·         Paste the below policy and click Save


{
       "Version": "2012-10-17",
       "Statement": [
             {
                    "Effect": "Allow",
                    "Principal": {
                           "AWS": "arn:aws:iam::XXXXX:user/mys3user"
                    },
                    "Action": "s3:PutObject",
                    "Resource": "arn:aws:s3:::mys3bucket/*"
             },
             {
                    "Effect": "Allow",
                    "Principal": {
                           "AWS": "arn:aws:iam::XXXXX:user/mys3user"
                    },
                    "Action": "s3:PutObjectAcl",
                    "Resource": "arn:aws:s3:::mys3bucket/*"
             }
       ]
}

This policy allows our IAM user to upload the file and set its access level to make it readable to everyone (public-read). We make the uploaded files publicly readable so the browser can show the card images directly from S3 when listing cards. If you don't want to make the images public, you can generate temporary signed image urls to show card images in the browser. You can find more information here.


3. Develop the signing part in server

First we create Spring controller to generate signature. The class use bucket name, bucket region, AWS credential for signing the bucket uploads and the secret of the AWS credential as variables. After generating the signature, the controller returns the signed upload data that will be used in upload request.

@RestController
public class CardUploadController {

       @Value("${user.card.upload.s3.bucket.name}")
       String s3BucketName;

       @Value("${user.card.upload.s3.bucket.region}")
       String s3BucketRegion;

       @Value("${user.card.upload.s3.bucket.awsId}")
       String s3BucketAwsId;

       @Value("${user.card.upload.s3.bucket.awsSecret}")
       String s3BucketAwsSecret;

       @RequestMapping(value = "/presign", method = RequestMethod.POST)
       @ResponseBody
       public PreSignedS3UploadData presignS3Upload(@RequestParam("contentType") String contentType, @RequestParam("fileName") String fileName, HttpSession session) {
             PreSignedS3UploadData res;
             try {
                    String extension = fileName.lastIndexOf('.') == -1 ? "" : fileName.substring(fileName.lastIndexOf('.'));
                    String s3FileName = "upload_" + (int)(100000 * Math.random()) + extension;
                   
                    res = S3SignUtil.generatePreSignedUploadData(s3BucketName, s3BucketRegion, s3BucketAwsId, s3BucketAwsSecret, contentType, s3FileName);
             }
             catch (Exception e) {
                    res = new PreSignedS3UploadData("Can't generate signature for upload: " + e.toString());
             }
             return res;
       }
 }

The signature generation algoritm first generates a security policy, then generates a signing key with AWS credentials and then signs the policy with the signing key. The security policy specifies the expiration date and time, ACL for the file being uploaded and some other options. This application generates a policy with 3 minute expiration time, public-read ACL to allow public access and 1MB max upload size. A sample policy looks like below.

{
       "expiration": "2017-04-26T23:09:59.638Z",
       "conditions": [
             { "acl": "public-read" },
             { "bucket": "XXXXX" },
             { "key": "upload_49921.jpg" },
             { "Content-Type": "image/jpeg" },
             ["content-length-range", 0, 1048576],
             { "x-amz-credential": "XXXXXXXX/20170426/eu-central-1/s3/aws4_request" },
             { "x-amz-algorithm": "AWS4-HMAC-SHA256" },
             { "x-amz-date": "20170426T000000Z" }
       ]
}

Policy and signature generation code is in S3SignUtil class.

For more information on generating the policy and signature, see here.


4. Prepare the web front end.

After we complete the code that generates the signed upload data, we can prepare the web front end. We will change the dashboard.jsp file and add a file upload input to the Add Card form. When the selection change in the file upload input, we generate a signature using the controller we created in the 3rd step. Then we generate a dynamic form to post the file with the signature to the S3 bucket url.

The script is below.

function cardImageFileUpdated(){ 
       var file = document.getElementById('cardImageInput').files[0];
      
       if (file != null)
             startCardImageFileUpload(file);
}

function startCardImageFileUpload(file) {
       $.ajax({
         type: "POST",
         url: "presign",
         data: 'contentType=' + encodeURIComponent(file.type) + '&fileName=' + encodeURIComponent(file.name),
         success: function(data){
                if (data.errorMessage)
                    alert(data.errorMessage);
               else
                    doCardImageFileUpload(file, data);
               },
       });
}


function doCardImageFileUpload(file, data){

       var formData = new FormData();
      
       formData.append('key', data.fileName);
       formData.append('acl', 'public-read');
       formData.append('Content-Type', data.contentType);
       formData.append('X-Amz-Credential', data.credential);
       formData.append('X-Amz-Algorithm', "AWS4-HMAC-SHA256");
       formData.append('X-Amz-Date', data.date);
       formData.append('Policy', data.policy);
       formData.append('X-Amz-Signature', data.signature);
       formData.append('file', $('input[type=file]')[0].files[0]);
      
       $.ajax({
           url: data.bucketUrl,
           data: formData,
           type: 'POST',
           contentType: false,
           processData: false,
           success: function () {
              var imageUrl = data.bucketUrl + "/" + data.fileName;
          
              document.getElementById('cardImagePreview').src = imageUrl;
              document.getElementById('cardImageUrl').value = imageUrl;
           },
           error: function () {
              alert("Upload error.");
           }
       });
}
  
And we change the Add Card form from

<form id="add-card-form" onsubmit="return false;">
       <input type="text" name="name" placeholder="name" />
       <button onclick="addCard()">Add</button>
</form>

to

<form id="add-card-form" onsubmit="return false;">
       <span>Card Name</span>
       <input type="text" name="name" placeholder="name" /><br/>
      
       <span>Card Image File</span>
       <input type="file" id="cardImageInput" accept="image/*" onchange="cardImageFileUpdated()"/>
       <img style="border:1px solid gray;height:160px;width:120px;" id="cardImagePreview" src="/images/default-card.png"/>
       <input type="hidden" id="cardImageUrl" name="imageUrl" value="/images/default-card.png"/> <br/>
                          
       <button onclick="addCard()">Add</button>
</form>

First we add an input with file type to select the image file. And we use an image tag to preview the image after the upload complete. And then we add a hidden imageURL field to Add Card form as I talked in the beginning of this post.

At this point, we finished the uploading the card image to S3 and saving the card with the url of the file uploaded to S3. Next we will show the image of the cards in card listing tables. We change the buildHtmlTable JavaScript function in dashboard.jsp from

if (cellValue == null) cellValue = "";
row$.append($('<td/>').html(cellValue));

to

if (cellValue == null) cellValue = "";
if (columnList[colIndex] == 'imageUrl')
 cellValue = '<img style="border:1px solid gray;height:160px;width:120px;" src="' + cellValue + '"/>';
row$.append($('<td/>').html(cellValue));

to use the imageURL field as card image.

After showing the card images in card listings, we have completed the changes. If you run the application with this command,

$ mvn spring-boot:run -Drun.jvmArguments="-Duser.activation.queue.name=XXX -Dmail.from.address=XXX -Duser.card.upload.s3.bucket.name=XXX -Duser.card.upload.s3.bucket.region=XXX -Duser.card.upload.s3.bucket.awsId=XXX -Duser.card.upload.s3.bucket.awsSecret=XXX"

you can use the application like the screenshots below.






Summary

In this post, I have shown adding a file upload functionality to add card images. I have used direct S3 uploads from the browser without uploading the file to a EC2 instance first. The code can be found at my GitHub repository.

In my next posts, I will continue to use various AWS services to add functionality to my digital card store application.


29 comments:

  1. Nice article, users are attracted when they see your post thanks for posting keep updatingAWS Online Training Hyderabad

    ReplyDelete
  2. Good job! Fruitful article. I like this very much. It is very useful for my research. It shows your interest in this topic very well. I hope you will post some more information about the software. Please keep sharing!!
    Web Designing Training Institutes in Chennai
    Web Designing Course in Chennai with placement
    website design courses
    web design classes
    Web Designing Training Centers in Chennai
    web design training chennai

    ReplyDelete
  3. Thanks for sharing this valuable information to our vision. You have posted a worthy blog keep sharing.

    dailyconsumerlife

    Guest posting sites

    ReplyDelete
  4. Thank you for your information.it is very nice article.
    AWS Online Training

    ReplyDelete
  5. Thank you for updating such an informative content. I Sugessed SLA for the Best Software Training in Chennai with 100% Placement. Offering Courses are AWS Training Course,Hardware and Networking Training, Advanced Excel Training, Machine Learning Training in Chennai, etc.,.

    ReplyDelete
  6. Really awesome blog. Your blog is really useful for me
    Regards,
    Data Science Course in Chennai

    ReplyDelete
  7. Your post is just outstanding !!! thanks for such a post, its really going great work.
    Data Science Training in Chennai | Data Science Course in Chennai

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Thanks for giving great kind of information. So useful and practical for me. Thanks for your excellent blog, nice work keep it up thanks for sharing the knowledge.
    AWS Training in Chennai | AWS Training Institute in Chennai

    ReplyDelete
  10. Clinical sas training in chennai | SAS Training course chennai
    I have to voice my passion for your kindness giving support to those people that should have guidance on this important matter.

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. Hey!Amazing work. With full of knowledge. Our Team resolve any glitches occurring while utilizing the software. Looking for solution of Quickbooks Error 1712 Contact us +1 877 751-0742 .Our experts will assist you to fulfil your accounting needs. The solutions are accurate and time-saving.

    ReplyDelete
  13. Hey! Nice Blog, I have been using QuickBooks for a long time. One day, I encountered QuickBooks Customer Service in my software, then I called QuickBooks Customer Servicet. They resolved my error in the least possible time.

    ReplyDelete