Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sdk upload tutorial #33

Merged
merged 15 commits into from
Oct 6, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/direct-upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,28 @@ following into the CORS field
(*it's basically saying that we are allowing GET,
POST and PUT requests from any Allowed Origin with any Allowed Header*)

+ Finally we need to add a policy to the bucket to make it public readable. Click
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jackcarlisle might be worth clarifying that this makes the files uploaded to the bucket viewable in browsers.

on the 'Add bucket policy' button and then add the following to the input field:

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


![save CORS](https://cloud.githubusercontent.com/assets/12450298/18393882/359e3bf6-76af-11e6-90da-bcd993d035ff.png)

#### Our bucket is now completely set up so that it will accept our POST request images!
Expand Down
334 changes: 334 additions & 0 deletions examples/sdk-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
# SDK Upload to S3 - A Complete Guide

We are going to implement a simple solution for uploading images to an S3 bucket
using the AWS SDK.

![upload example](https://cloud.githubusercontent.com/assets/12450298/18955390/2996f122-864f-11e6-92f5-f2b4c631b2d7.png)

### Contents
- [Creating an S3 Bucket](#step-1---creating-the-bucket)
- [Creating IAM User with S3 Permissions](#step-2---creating-an-iam-user-with-s3-permissions)
- [AWS SDK Configuration](#step-3---configure-the-aws-adk)
- [Implement the SDK](#step-4---implement-the-sdk)
- [Create a Server](#step-5---create-a-server-to-handle-our-upload-file)
- [Client Side Code](#step-6---write-the-client-side-code-to-send-our-file-to-the-server)
- [Take it for a Spin](#take-it-for-a-spin)
- [Learning Resources](#learning-resources)

### Step 1 - Creating the bucket

In our direct-upload example we walk through the steps of how to create a new
bucket in S3. Check out [the tutorial](https://github.com/dwyl/image-uploads/blob/master/examples/direct-upload/README.md#step-1---creating-the-bucket)
and follow the steps.

### Step 2 - Creating an IAM user with S3 permissions

Just as in Step 1 we've already created a step-by-step process for creating a
new IAM user that has the correct permissions to access the bucket you created
[above](#step-1---creating-the-bucket). Take a look at [the tutorial](https://github.com/dwyl/image-uploads/blob/master/examples/direct-upload/README.md#step-2---creating-an-iam-user-with-s3-permissions)
and again follow the steps.

### Step 3 - Configure the AWS SDK

In order to upload to our S3 bucket, we have to use the AWS SDK. Install it using
the following command:

`$ npm install aws-sdk --save`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we tend to exclude the $ from shell/terminal commands to avoid confusion.


Next we'll need to configure our AWS SDK. You can do this in a number of ways.
We're going to create a credentials file at `~/.aws/credentials`. To do this we
take the following steps:

* `$ cd` this takes us back to the root of our file system
Copy link
Member

@nelsonic nelsonic Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend people open a _new terminal window_ to perform these actions so they can close it at the end and go back to their project directory in the original terminal. ...

Tell people to pwd first so they can get back to their project's working directory after they set up their ~/.aws/credentials file ...

* `$ mkdir .aws` creates a folder that will hold our credentials file
* `$ cd mkdir` navigate to our `.aws` directory
* `$ touch credentials` creates our credentials file
* Open your credentials file in your text editor and add the following:
```
[default]
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_key
```
* save the file

The credentials should be the ones associated with the IAM user you created in
[Step 2]((#step-2---creating-an-iam-user-with-s3-permissions)).

#### You will now be able to use the AWS SDK!

### Step 4 - Implement the SDK

First you'll need to set some environment variables. These are as follows:

Copy link
Member

@nelsonic nelsonic Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip: include links to "learn-xyz" tutorials when mentioning concepts that might not be familiar to everyone (new developers) in this case:

If you are new to Environment Variables see: github.com/dwyl/learn-environment-variables

Also, you could recommend that people store these two in an .env file in their project.

`export S3_REGION=<YOUR_REGION>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer all AWS-based Environment Variables to be prefixed with AWS_ e.g: AWS_S3_REGION ... yes, it's quite safe to assume that there's only one env var called S3_REGION but no harm in being explicit.

`export S3_BUCKET=<YOUR_BUCKET_NAME>`

Create a `src` directory in the root of your project. Create a file within this
directory called `upload.js`. This file will contain our SDK functionality.
Add the following to this file:

```js
// load the AWS SDK
var AWS = require('aws-sdk')
// crypto is used for our unique filenames
var crypto = require('crypto')
// we use path to add the relative file extension
var path = require('path')

// assign our region from our environment variables
AWS.config.region = process.env.S3_REGION

/**
* Returns data specific to our upload that we send to the front end
* @param {Buffer} file - file that we are uploading
* @param {string} filename - name of the file to be uploaded
* @param {function} callback
**/
function upload (file, filename, callback) {
// creating our new filename
var filenameHex =
filename.split('.')[0] +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some people use . (dots) in their filenames e.g: my.amazing.holiday.photo.jpg
which would end up my.HEXSTRING.jpg so there's a relatively low risk of filename collision. but given that you are getting the extname below, you could:

var ext = '.' + path.extname(filename);
var filenameHex = filename.replace(ext, '') +
   crypto.randomBytes(8).toString('hex') + ext;

crypto.randomBytes(8).toString('hex') +
path.extname(filename)
// loading our bucket name from our environment variables
var bucket = process.env.S3_BUCKET
// creating a new instance of our bucket
var s3Bucket = new AWS.S3({params: {Bucket: bucket}})
// sets the params needed for the upload
var params = {Bucket: bucket, Key: filenameHex, Body: file}
// SDK upload to S3
s3Bucket.upload(params, function (err, data) {
if (err) console.log(err, data)
Copy link
Member

@nelsonic nelsonic Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handling errors in hapi apps: https://github.com/dwyl/hapi-error#handleerror-everywhere
this could be:

var handleError = require('hapi-error').handleError;

s3Bucket.upload(params, function (err, data) {
  handleError(error, data); // let hapi-error display the appropriate error message
  return callback(err, result);
}

// callback with the data that gets returned from S3
else callback(null, data)
})
}

module.exports = {
upload
}
```

#### We've now set up our S3 upload function!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be a an h4 (sub-headding) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel as though it helps to separate the sections. I can remove it if you'd like



### Step 5 - Create a server to handle our upload file

Create a directory called `lib`. This will hold our server + routes.

Create a file called `index.js` and add the following:

```js
'use strict'

var Hapi = require('hapi')
var Inert = require('inert')
var assert = require('assert')

// creating a new instance of our server
var server = new Hapi.Server()

// connecting to port 8000
server.connection({port: 8000})
// registering the Inert module which allows us to render static files
server.register([Inert], function (err) {
assert(!err) // not much point continuing without plugins ...

// adding our routes (we'll be creating this file next)
server.route(require('./routes.js'))

// starting the server
server.start(function (err) {
assert(!err) // not much point continuing if the server does not start ...
console.log('The server is running on: ', server.info.uri)
})
})

module.exports = server
```

Next create a file called `routes.js` and add the following:

```js
// require the function we just created
var s3 = require('../src/upload.js')
// path is needed to resolve the filepath to our index.html
var path = require('path')

module.exports = [
// our index route
{
method: 'GET',
path: '/',
handler: function (request, reply) {
return reply.file(path.resolve(__dirname, '../public/index.html'))
}
},
// our endpoint
{
method: 'POST',
path: '/file_submitted',
handler: function (request, reply) {
// we'll be sending through a file along with it's filename
var file = request.payload.file
var filename = request.payload.filename
// upload the file to S3
s3.upload(file, filename, function (err, data) {
if (err) console.log(err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you can do request.handleError(err, err);
(instead of having an if statement and having to write a another test for it...)

// send the data back to the front end
reply(data)
})
}
},
// this is for our static files
{
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: path.resolve(__dirname, '../public'),
listing: true,
index: false
}
}
}
]
```

### Step 6 - Write the client side code to send our file to the server

Create a new directory called `public`. Create an `index.html` file within this
directory. Add the following to the file:

```html
<!DOCTYPE html>
<html>
<head>
<title>S3 Upload Demo</title>
<link rel="icon" href="http://downloadicons.net/sites/default/files/upload-icon-46097.png">
<!-- optional stylesheet -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flat-ui/2.2.2/css/flat-ui.min.css">
<!-- link to our client side js file that we will create next -->
<script type="text/javascript" src="client.js"></script>
</head>
<body>
<h1>S3 Direct Upload Demo</h1>
<br/>
<!-- simple form with a file input -->
<form>
<input class="form-control input-sm" id="fileInput" type="file" name="file" onchange="uploadDemo.saveFile(this.files)"/>
</form>
<br/>
<!-- submit button that will trigger our upload -->
<button id="submit" class="btn btn-embossed btn-primary" onclick="uploadDemo.submitFile()">Submit</button>
<!-- this will hold the link to our newly uploaded image -->
<div class="successMessageContainer">
<a class="imageLink"></a>
</div>
</body>
</html>
```

Create the javascript file that we're including in our html file and call it
Copy link
Member

@nelsonic nelsonic Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jackcarlisle this works but requires JS... we can do better (progressive enhancement) ... 😉
It's totally my fault for not being specific when we discussed the requirements.
Please create an issue in the backlog to add a progressive enhancement version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nelsonic issue added!

`client.js`. Add the following to the file:

```js
// IIFE that will contain our global variables
var sdkDemo = (function () {
// file that we want to upload
var file
// filename of that file
var filename

/**
* Saves our file and filename to the relevant global variables
* @param {Buffer} uploadFile - file that we are uploading
**/
function saveFile (uploadFile) {
file = uploadFile[0]
filename = file.name
}

/**
* Calls our sendFileToServer function
**/
function submitFile () {
sendFileToServer(file, filename)
}

/**
* Sends file to server and returns link to uploaded file
* @param {Buffer} file - file that we are uploading
* @param {string} filename - name of the file to be uploaded
**/
function sendFileToServer (file, filename) {
// creates a new FormData instance so we can send our image file
var formData = new FormData()
// append the file to the formData
formData.append('file', file)
// append the filename to the formData
formData.append('filename', filename)
// create a new XHR request
var xhttp = new XMLHttpRequest()
// wait for a 200 status (OK)
xhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
// save the file location from the responseText
var fileLocation = JSON.parse(xhttp.responseText).Location
// add success message to index.html
var successMessage = document.createElement('h4')
successMessage.innerHTML = 'Image Successfully Uploaded at: '
// create a link to the image
var imageATag = document.querySelector('a')
var link = fileLocation
// set the link to the image location
imageATag.setAttribute('href', link)
var imageLink = document.createElement('h4')
imageLink.innerHTML = link
var div = document.querySelector('div')
// add the success message and image link to the index.html
div.insertBefore(successMessage, div.firstChild)
imageATag.appendChild(imageLink)
}
}
// open the POST request
xhttp.open('POST', '/file_submitted', true)
// send the formData
xhttp.send(formData)
}

return {
// make your functions available to your index.html
saveFile,
submitFile
}
}())
```
#### You can now upload to S3 directly from your server!

### Take it for a spin

+ In your terminal run the following command to start the server:
`$ node lib/index.js`

+ Navigate to localhost:8000. You should see the following screen. Click on **Choose File**:

![upload](https://cloud.githubusercontent.com/assets/12450298/18966154/0b014fae-8678-11e6-83b1-523bea2c605f.png)

+ Once you've selected a file, click the 'Submit' button:

![submit](https://cloud.githubusercontent.com/assets/12450298/18966222/5ca5687c-8678-11e6-99b0-aa798222bd80.png)

+ If your upload has been successful you should see the success message with the
link to your image. Click on the link:

![success](https://cloud.githubusercontent.com/assets/12450298/18966287/8f3a3484-8678-11e6-90e4-2c8147b4907d.png)
![click link](https://cloud.githubusercontent.com/assets/12450298/18966895/041c8e08-867b-11e6-89e0-183ff2f19cb8.png)

+ Click on the download and you should see your image:

![image](https://cloud.githubusercontent.com/assets/12450298/18966940/367afb3c-867b-11e6-8c0e-f66f505027c2.png)

+ Navigate back to your S3 bucket. You should see your image there:

![image in bucket](https://cloud.githubusercontent.com/assets/12450298/18966358/d14fa160-8678-11e6-84aa-1420b5ec6621.png)
21 changes: 21 additions & 0 deletions examples/sdk-upload/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

var Hapi = require('hapi')
var Inert = require('inert')
var assert = require('assert')

var server = new Hapi.Server()

server.connection({port: 8000})
server.register([Inert], function (err) {
assert(!err) // not much point continuing without plugins ...

server.route(require('./routes.js'))

server.start(function (err) {
assert(!err) // not much point continuing if the server does not start ...
console.log('The server is running on: ', server.info.uri)
})
})

module.exports = server
Loading