40

Upload to Amazon S3 with Uploadify

Posted March 20th, 2011 (Updated 7 Nov 2011) in Javascript, PHP

Update Jul 02 2011: crossdomains.xml should be crossdomain.xml

Update Jun 29 2011: Updated S3 upload URL – no more security error

If you’ve ever wanted to allow people on your website to upload files to Amazon S3, you’ll know the only real way to do this is with flash – as it allows the client to upload directly instead of funneling all traffic through your server. You could write your own script for this, however there’s already a nifty little prebuilt tool I’m sure you’ve already heard of called Uploadify which can not only do the job for you, it can do so with great flexability.

For the lazy: Download the entire script here

You’ll need the following things:

Set up the bucket

Firstly set up your bucket. I’ve called mine bucket.mysite.com. Inside the bucket needs to be a file called crossdomain.xml which is required to upload files to S3. Here is the contents:

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-access-from domain="*" secure="false" />
</cross-domain-policy>

Make sure this file is readable by everyone.

Create your site

Create a folder for your site in your web directory (I’m calling mine S3Uploader) and inside create includes and files directories. Drop both the jquery.js file and your uploadify folder into files.

Here is the code for index.php (to go in your S3Uploader directory):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
	//System path for our website folder
	define('DOCROOT', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR);
	//URL for our website
	define('WEBROOT', htmlentities(
		substr($_SERVER['REQUEST_URI'], 0, strcspn($_SERVER['REQUEST_URI'], "nr")),
		ENT_QUOTES
	));
 
	//Which bucket are we placing our files into
	$bucket = 'bucket.mysite.com';
	// This will place uploads into the '20100920-234138' folder in the $bucket bucket
	$folder = date('Ymd-His').'/'; //Include trailing /
 
	//Include required S3 functions
	require_once DOCROOT."includes/s3.php";
 
	//Generate policy and signature
	list($policy, $signature) = S3::get_policy_and_signature(array(
		'bucket' 		=> $bucket,
		'folder'		=> $folder,
	));
?>
<html>
<head>
<title>test Upload</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="files/uploadify/uploadify.css" />
<script type='text/javascript' src="files/jquery.js"></script>
<script type='text/javascript' src="files/uploadify/swfobject.js"></script>
<script type='text/javascript' src="files/uploadify/jquery.uploadify.v2.1.4.min.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$("#file_upload").uploadify({
			'uploader'		: '<?= WEBROOT ?>files/uploadify/uploadify.swf',
			'buttonText'		: 'Browse',
			'cancelImg'		: '<?= WEBROOT ?>files/uploadify/cancel.png',
			'script'		: 'http://s3.amazonaws.com/<?= $bucket ?>',
			'scriptAccess'		: 'always',
			'method'		: 'post',
			'scriptData'		: {
				"AWSAccessKeyId"		: "<?= S3::$AWS_ACCESS_KEY ?>",
				"key"				: "${filename}",
				"acl"				: "authenticated-read",
				"policy"			: "<?= $policy ?>",
				"signature"			: "<?= $signature ?>",
				"success_action_status"		: "201",
				"key"				: encodeURIComponent(encodeURIComponent("<?= $folder ?>${filename}")),
				"fileext"			: encodeURIComponent(encodeURIComponent("")),
				"Filename"			: encodeURIComponent(encodeURIComponent(""))
			},
			'fileExt'		: '*.*',
			'fileDataName' 		: 'file',
			'simUploadLimit'	: 2,
			'multi'			: true,
			'auto'			: true,
			'onError' 		: function(errorObj, q, f, err) { console.log(err); },
			'onComplete'		: function(event, ID, file, response, data) { console.log(file); }
		});
	});
</script>
</head>
<body>
 
	<div align='center'>
		<input type='file' id='file_upload' name='file_upload' />
	</div>
 
</body>
</html>

A breakdown: At the top I set up some convenience definitions as well as defining the name of the bucket we’re uploading to ($bucket variable) and the folder within your bucket. The $folder variable is useful for avoiding overwriting of files when uploading one with the same filename, however it is not required. I then included the required stylesheet and script files (You can customize the stylesheet to change the way transfers look) and set up uploadify.

The scriptData object is the most important part here. That’s the upload data being sent to S3. You shouldn’t need to change anything in this section and doing so may cause uploads to fail.

One very common issue with using Uploadify to upload to S3 is that it will seemingly randomly fail to upload and instead return a ’403′ error. This is due to a problem with actionscript encoding ‘+’ characters in the signature incorrectly. To avoid this, I’ve written a get_policy_and_signature function in my S3 class (code below) to recursively regenerate until a signature without this character is made – a bandaid that should fix the problem. Here’s includes/s3.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
class S3 {
	public static $AWS_ACCESS_KEY 		= '< Your access key >';
	public static $AWS_SECRET_ACCESS_KEY 	= '< Your secrete key >';
 
	/*
	 * Purpose:
	 * 		Actionscript encodes '+' characters in the signature incorrectly - it makes
	 * 		them a space instead of %2B the way PHP does. This causes uploadify to error
	 * 		out on upload. This function recursively generates a new policy and signature
	 * 		until a signature without a + character is created.
	 * Accepts: array $data
	 * Returns: policy and signature
	 */
	public static function get_policy_and_signature( array $data )
	{
		$policy = self::get_policy_doc( $data );
		$signature = self::get_signature( $policy );
 
		if ( strpos($signature, '+') !== FALSE )
		{
			$data['timestamp'] = intval(@$data['timestamp']) + 1;
			return self::get_policy_and_signature( $data );
		}
 
		return array($policy, $signature);
	}
 
	public static function get_policy_doc(array $data)
	{
		return base64_encode(
			'{'.
				'"expiration": "'.gmdate('Y-m-dTH:i:sZ', time()+60*60*24+intval(@$data['timestamp'])).'",'.
				'"conditions": '.
				'['.
					'{"bucket": "'.$data['bucket'].'"},'.
					'["starts-with", "$key", ""],'.
					'{"acl": "authenticated-read"},'.
					//'{"success_action_redirect": "'.$SWFSuccess_Redirect.'"},'.
					'{"success_action_status": "201"},'.
					'["starts-with","$key","'.str_replace('/', '/', $data['folder'] ).'"],'.
					'["starts-with","$Filename",""],'.
					'["starts-with","$folder",""],'.
					'["starts-with","$fileext",""],'.
					'["content-length-range",0,5242880]'.
				']'.
			'}'
		);
	}
 
	public static function get_signature( $policy_doc ) {
		return base64_encode(hash_hmac(
			'sha1', $policy_doc, self::$AWS_SECRET_ACCESS_KEY, true
		));
	}
}

This should be all there is to it. Navigate to the S3Uploader folder in your browser and you’ll be presented with a single Browse button.

Download the full script here.

  • Bill

    Hi,

    Firstly, thanks for the great script.

    I have followed all your instructions and keep having the “Security Error” from the uploader. I am pretty sure the crossdomains.xml is nicely setup. the $AWS_ACCESS_KEY and $AWS_SECRET_ACCESS_KEY is working in my other S3 php SDK project. I have just changed the bucket and folder names to mine.

    Any more clues you can provide? Thank you.

  • Flynsarmy

    Bill,

    You could use wireshark to determine exactly what the error is. Amazon always sends extra information along with their response XML file however Uploadify doesn’t give you the option of accessing it. I’ve never done this before though.

    Alternatively I’ve found the most common reasons this occurs is due to me messing with the uploadify constructor arguments. I believe what you add in there is also sent in the $post to Amazon and if your security policy doesn’t match exactly you’ll get the error.

    Sorry I couldn’t be more help.

    EDIT: You might also try updating the S3 class http://undesigned.org.za/2007/10/22/amazon-s3-php-class

  • Bill

    Hi Flynsarmy,

    Thanks for amazingly prompt respose.

    I have figured out the security error. It was a stupid one as the crossdomain.xml (should have without “s”) is wrongly spell.

    now i get another error “HTTP Error”

    any clues?

    • Flynsarmy

      Sorry, all I can give is the above. I got HTTP and Security errors quite alot writing this tutorial too, so it could be any of a large number of things. I’d advise using wireshark – it should help you figure out exactly what’s going on.

  • Bill

    Dear Flynsarmy,

    Thanks so much your assistance.

    I will post back to this blog once i have idea. Thanks.

  • Chris

    Might be broken on files more than a few MBs. I get the same HTTP error if I upload anything more than a small photo.

  • Nathan

    awesome script, thank you for posting it! works perfectly :) I needed to get an s3 uploader working fast for a client and none of the other examples I found worked.

    @Chris – in includes/s3.php change the content-length-range on line 45 to a bigger value. the default max size is 5242880 (only 5mb). I’m using this to upload videos, so I changed it to 314572800 (300mb)

    • Flynsarmy

      Happy to help :)

    • Brad

      THANK YOU!!! Was taking me forever to track down this error and figure out why the uploads were being limited to 5MB.

      Great script!!!

  • Roberto

    Hi,
    thanks for this updated post.
    I’m trying it out, but still get a Security Error message.
    My bucket is read-write-full control to everyone, authenticated and owner (just for these tests).
    I use your zip but what I don’t understand if your S3 class is all I need or I should you Amazon S3 PHP Class somewhere else.

    • Roberto

      another question: is the xml crossdomain file name correct?
      Is it crossdomain.xml or crossdomains.xml?
      (found crossdomain.xml on other sites)
      (I’ve put it in the bucket folder.)

      • Flynsarmy

        Hey Roberto,

        It’s crossdomain.xml. I’ve updated my post accordingly – good catch!

  • Tom

    I am also getting “HTTP ERROR”

    Can you please tell me exactly which lines in your index.php file I am supposed to change? It’s a little confusing.

    For instance, do I change only line 11 or also line 20 to reflect the bucket? Currently I have changed only line 11 as follows:

    $bucket = ‘http://dr8fvfrm418kp.cloudfront.net’;

    And I have left lines 20 and 21 as follows:

    ‘bucket’ => $bucket,
    ‘folder’ => $folder,

    Thank you for clearing this up for me, and for the excellent article.

    Tom

    • Flynsarmy

      Hey Tom,

      Unfortunately Uploadify makes debugging these issues pretty difficult. What you have is correct – only line 11 changes. Have you created your crossdomain.xml file? That’s about the only help I can really give.

      Good luck with your webapp!

  • Chris

    Hey,

    I’m trying to get this working in wordpress and am having some trouble. I think it’s related to the php before the initial html tag in the index.php. I can’t figure out how to implement this.

    Thanks in advance for help (i’d be happy to make a donation for some help).

    Thanks, Chris

    • Chris

      I wound up using an i-frame embed and is working now.

  • alan

    i was just wondering if there’s a way to rename the uploaded file. that way you don’t have to make a million folders to ensure someone doesn’t upload over their file with the same name.

    thanks for the great script!
    alan

    • Flynsarmy

      As far as I’m aware there isn’t. You’ll need to upload to your server and then forward to S3. That could potentially amount to alot of bandwidth though

      • alan

        thanks for the quick reply! Basically to generate unique file names I just strung the $folder to ${filename}. Also, I was trying to change the acl from “authenticated-read” to “public-read” but it gives me an HTTP:Error. Just wondering if you know how to fix this.

        thanks!
        Alan

        • alan

          disregard that last comment…i didn’t change the “authenticated-read” to “public-read” in the s3.php

  • jecz

    This code is crazy good! Well done. Thank you.

  • Stuart Ridout

    Brilliant resource. Thank you very much.

    I just have one issue … onComplete in IE9. I am writing an entry to a mySQL database but onComplete does not fire in IE.

    It works great in Firefox and in Chrome but not IE.

    Has anybody else experienced this?

  • Steve M.

    I was just trying this out, but the “WEBROOT” variable seems to not work properly. Am I missing something?

    [20:01:25.767] GET http://x.compute-1.amazonaws.com/S3Uploader/%3C?=%20$WEBROOT%20?%3Efiles/uploadify/uploadify.swf [HTTP/1.1 404 Not Found 103ms]

    • Steve M.

      Well… Seems like my problem is that the php option short_open_tag isn’t enabled. It says it is enabled by default but it’s not. Not sure why this version of php (5.3.6) isn’t working… replaced all the <?= with <?php echo and now it works.. kinda… Getting some security errors now. I'll read again since I've seen something about this.

      • Steve M.

        It’s working now! Alright. So short_open_tag=Off was my biggest issue since the variables would not expand. My second biggest issue is that the 2 test S3 account I had was using illegal characters. The first had a capital “I” in the name, which made the DNS resolver fail. The second was using a “_” in it. So stay with lowercase letters and numbers. Uploadify and this script won’t work if the names aren’t right even if you change the path. although the file crossdomail.xml exists at the root of s3.amazonaws.com which could make it work.

        But I’m happy now.

  • Mayank

    Awesome post.

    I have a question here. Is line no 42 a security risk “AWSAccessKeyId” : “”

    Will user be able to see the AWSAccessKeyId if he does View Source?

    • Flynsarmy

      Mayank,

      They’ll be able to see your AWSAccessKeyId by viewing page source, however they won’t be able to see your secret key. Think of it as a username and password. There shouldn’t be any risk here.

  • AlixcaN

    Firstly, thanks for this great script.
    But i’m wonderin how we can show links in div id=”links” when upload(s) completed

  • Iam ubale to upload more than 300 mB using s3

    can any one help me that i can upload the video file more than 300 MB.so when i was trying to run im local machine to upload the video into the s3 server it showing some error like
    Fatal error: Out of memory (allocated 413663232) (tried to allocate 401604608 bytes) in C:\xampp\htdocs\dev\app\controllers\components\s3.php on line 24

    so can anyone help me regarding this

    Thanks
    Ravi

  • Fabian

    This works for smaller files, but when I upload larger files 40MB or more it says it completes, but the file isn’t actually uploaded.
    Any ideas?

  • Kurt Reinholtz

    THANK YOU for the solution. Worked for me right out of the box. Been digging online for something that would do direct from browser to S3 bucket. Nicely done.

  • Chris K

    On OS X works fine in Chrome but not Safari – just hangs when trying to upload the file.

    Any ideas?

  • Michael W

    Has anyone had any luck implementing this with Uploadify 3? It seems they’ve changed a ton of things with their method calls and this method no longer works.

  • Ben C

    The code you provided is for a much earlier version of Uploadify than exists now, and the parameters have changed enough that I have no real idea how to convert your code to the latest Uploadify (3.1).

    Do you have any working code that implements the AWS direct upload using Uploadify 3.1? I know this has been asked last month, but I cannot find any definitive code for this anywhere on the internet.

    If ANYONE knows of a complete PHP implementation of this for v3.1, i know plenty of developers would be very thankful, myself included.

  • Martin

    Any luck with version 3 anyone?

  • rondiege

    AWESOME! Thank you flynsarmy, exactly what I needed. And thank you @Nathan for the hint about the HTTP Error for large files.

  • http://www.facebook.com/sangeetha.narayanamoorthy Sangeetha Narayana Moorthy

    Very Useful.. Simple code thanks.. I need to delete the uploaded file .. how to do?

  • Sam Tandy

    This is a great script, thanks very much. For those receiving ‘HTTP error’ on large file uploads – make sure your php settings allow large file uploads (http://www.sitepoint.com/upload-large-files-in-php/), also add the ‘sizeLimit’ value to your uploadify script and increase the ‘content-length-range’ max-value in the s3.php file.

    I need to add in file validation for file-type, and also trigger server-side php scripts to update database data once the queue has been completed. However, Uploadify’s ‘onQueueComplete’ command doesn’t seem to trigger when added to this script. Anyone else found a way to do this?

  • Sam Tandy

    If anyone knows a similar script that’s been converted to work with Uploadify versions 3+ that would be very handy. I’m currently trying to upgrade it as there doesn’t seem to be any existing documentation for this version 2.1.4.

    Also, with the existing script, you can trigger a server-side script after each upload by adding an AJAX call to ‘onComplete’ such as…

    $.ajax({
    type: “POST”,
    url: “url/to/post/to.php”,
    data: “uploadComplete=true&file=” + file.name
    });

    …where data can be used to build the querystring of data to pass to the specified url.

  • Adam

    how do we set the content type? dynamically.

    meaning – the user uploads a .png it gets that content-type for the s3 object
    and if he uploads a .jpg then that content-type gets applied