Visit The School

Uploading Images To AWS S3 In Swift With A Node Backend

Alexander Paterson
|
Posted about 1 year ago
|
15 minutes
Uploading Images To AWS S3 In Swift With A Node Backend
Handling image uploads in any application is notoriously tricky

If this content is a bit heavy for you: take a look at my course on mobile backend development.

Many apps require some sort of image-upload functionality, but implementing this means solving a few different problems. The developer must decide where the images are going to be stored, and how they're going to get there securely. Using a remote storage solution is a must; you don't want to waste your server's bandwidth or disk speed on media.

Achieving this functionality in an asynchronous web application or a mobile app poses another issue: if authentication is done through the server, do we really have to post our images to our server, only to then have them moved to a permanent location?

Amazon S3

Amazon Simple Storage Service (S3) can solve all these problems for us. S3 buckets are units of storage for storing files. In this guide, I'm going to show you how to use a Node.js backend to issue signed URL's to authenticated users, each of which will allow our Swift application to upload a single file directly to the S3 bucket. The uploaded image will be available at its own URL.

Overview

First, I'll show you how to set up your bucket. Then, I'll write a minimal Express application that generates signed URL's -- you can implement the authentication yourself (see my course on mobile backend development). Finally, I'll show you how to use Alamofire to retrieve the signed URL, and then upload an image using it.

Setting Up Your Bucket

First, sign up for Amazon web services, and navigate to your S3 dashboard. Select Create Bucket and enter a name; I usually use a domain name. Set the region to Oregon - this corresponds to the region "us-west-2", which I'll be referring to in our configuration later. Click Create.

Creating A New Bucket

Now, on the bucket's dashboard, select Properties in the top right, then, under Permissions, hit Add bucket policy. Bucket policies are comprised of some cryptic-looking JSON, but the policy we're adding here just allows anybody to view any of the files in our bucket.

Adding An S3 Bucket Policy

Copy and paste the following as the bucket policy, and click Save.

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::<YOUR_BUCKET_NAME>/*"
    }
  ]
}

Now we just need to create a secret key which will allow our backend to generate signed URL's. Navigate to your IAM dashboard by selecting Security Credentials from the account dropdown menu in the top right.  

AWS Account Dropdown menu

Select Users from the side-menu, and click Create New Users. Come up with a username for the new user, and enter it in the first input field; I usually use <domain name>-s3 as a username. Then click Create. Now click Show User Security Credentials and copy and paste them into somewhere safe -- don't lose these credentials, or you'll have to create a new user. You must also never leak them: keep them out of your source code.

AWS Access Keys

Now click Close (twice) and select your new user from the list. Select Permissions and hit Attach Policy.

IAM User Dashboard

Tick AmazonS3FullAccess and click Attach Policy.

AmazonS3FullAccess

This gives this user the ability to perform any action on any of our S3 buckets. Our applications do this using the credentials we copied before.

It's time to write our Node backend that will issue signed URL's.

Node Backend

For this section, I'm just going to assume you know how to use Node and Express, and throw a bunch of code at you. The only idiosyncratic module I'm using here is aws-sdk, which gives us access to a method for generating signed S3 URL's with our secret key, which I'll store in a file called config.js.

In a real application you'll seperate all this code out into a router and a controller, and you'll want to have some authentication required before signed URL's are actually issued.

// index.js

var express = require('express');
var bodyParser = require('body-parser');
var morgan = require('morgan');
var uuid = require('uuid');
var aws = require('aws-sdk');

var config = require('./config');

var s3 = new aws.S3();
s3.config.update({accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey});

function getSignedURL(req, res, next) {
  var params = {
    Bucket: 'xxx', // your bucket name
    Key:  uuid.v4(), // this generates a unique identifier
    Expires: 100, // number of seconds in which image must be posted
    ContentType: 'image/jpeg' // must match "Content-Type" header of Alamofire PUT request
  };
  s3.getSignedUrl('putObject', params, function(err, signedURL) {
    if (err) {
      console.log(err);
      return next(err);
    } else {
      return res.json({postURL: signedURL, getURL: signedURL.split("?")[0]});
    }
  });
}

// Standard Express Stuff

var app = express();
var router = express.Router();

router.route('/get_signed_url')
  .get(getSignedURL);

app.use(morgan('combined'));
app.use(bodyParser.json());
app.use('/v1', router);

var port = process.env.PORT || 3000;
var host = process.env.HOST || '127.0.0.1';

console.log("Listening on", host, port);
app.listen(port, host);

config.js looks like this:

// config.js

module.exports = {
  accessKeyId: 'AXXXXXXXXXXXXXXXXXXXXA',
  secretAccessKey: 'PXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo',
  region: "us-west-2" // important
}

and for good measure, make sure you keep your secret key (along with your node_modules/ directory) out of version control with a .gitignore file:

# .gitignore

node_modules/
config.js

Now, after initialising your node package and installing the necessary modules, if you run your application with node index.js, and navigate to http://localhost:3000/v1/get_signed_url you should see some JSON with postURL and getURL properties. Awesome.

Time for some swift.

Uploading Images In Swift

Check out the Alamofire repository and follow its install instructions. We're also going to be using a module called SwiftyJSON, so install that pod too. This is what my Podfile looks like:

# Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

target 'XcodeProjectName' do
    pod 'Alamofire', '~> 3.4'
    pod 'SwiftyJSON'
end

Once you have those installed, we also need to enable insecure HTTP communication in our Xcode project. Right click on info.plist and click Open As... > Source Code. Add the following highlighted lines to the bottom of the file:

NSAllowsArbitraryLoads

Now our Xcode project is ready for some code. Create a new Swift file in your Xcode project called ImagePost.swift and copy and paste in the following code:

//
//  ImagePost.swift
//  Uploading Images To AWS S3 In Swift With A Node Backend
//

import Alamofire
import SwiftyJSON

class ImagePost {
    
    static let getTokenURL = "http://localhost:3000/v1/get_signed_url"
    
    private static func performUpload(image: UIImage, postURL: String, getURL: String, completionHandler: (success:Bool, getURL:String?) -> ()) {
        if let imageData = UIImageJPEGRepresentation(image, 0.1) { // 0.1 for high compression
            print("Uploading! Hang in there...")
            let request = Alamofire.upload(.PUT, postURL, headers: ["Content-Type":"image/jpeg"], data:imageData)
            request.validate()
            request.response { (req, res, json, err) in
                if err != nil {
                    print("ERR \(err)")
                    // dispatch compltionHandler to main thread (background processes
                    // should never manipulate the UI, and completionHandler will
                    // probably include a segue, or something)
                    dispatch_async(dispatch_get_main_queue(), {
                        completionHandler(success:false, getURL: getURL)
                    })
                } else {
                    dispatch_async(dispatch_get_main_queue(), {
                        completionHandler(success:true, getURL: getURL)
                    })
                }
            }
        }
        
    }
    
    static func uploadImage(image: UIImage, completionHandler: (success:Bool, getURL: String?) -> ()) {
        let request = Alamofire.request(.GET, ImagePost.getTokenURL, encoding: .JSON)
        request.validate()
        request.responseJSON { response in
            switch response.result {
            case .Success:
                if let value = response.result.value {
                    let json = JSON(value)
                    if let postURL = json["postURL"].string, getURL = json["getURL"].string {
                        print(postURL)
                        performUpload(image, postURL: postURL, getURL: getURL, completionHandler: completionHandler)
                        return
                    }
                }
                completionHandler(success: false, getURL: nil)
            case .Failure (let error):
                print("ERR \(response) \(error)")
                completionHandler(success: false, getURL: nil)
            }
        }
    }
}

Now from anywhere in this Xcode project you should be able to call ImagePost.uploadImage, passing in a UIImage, and a completion handler which takes two arguments: a boolean indicating whether or not the upload was successful, and an optional String at which the image can now be found at. 

The code above should be pretty easy to understand for anybody with swift experience.

Now I'll just paste the simplest usage example I can think of. This is just a ViewController that pops-up an image picker, and when you select an image it uploads it to S3, and logs the URL of the image. If you created a new Xcode project that was a simple single-view application, you can simply copy and paste the following code into ViewController.swift and run the simulator.

The following code is executed top-to-bottom, with imagePickerController didFinishPickingWithInfo being called only after the image is selected.

//
//  ViewController.swift
//  Uploading Images to AWS S3 In Swift With A Node Backend
//

import UIKit
import Alamofire
import SwiftyJSON

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    let imagePicker = UIImagePickerController()
    var image: UIImage?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        imagePicker.delegate = self
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .PhotoLibrary
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(true)
        
        presentViewController(imagePicker, animated: true, completion: nil)
    }
    
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
        if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
            self.image = pickedImage
        }
        dismissViewControllerAnimated(true, completion: nil)
        
        
        if let image = self.image {
            ImagePost.uploadImage(image, completionHandler: { (success, getURL) in
                if (success) {
                    print(getURL)
                } else {
                    print("Couldn't upload :(")
                }
            })
        }
    }
}

And that's all. If you had any issues comprehending any of this code, just leave a comment below; it should be pretty straightforward though.



ALEXANDER
PATERSON