├── .hgignore ├── LICENSE ├── README.rst ├── setup.py └── uploadify_s3 ├── __init__.py ├── templates ├── uploadify_head.html ├── uploadify_upload.html └── uploadify_widget.html ├── templatetags ├── __init__.py └── uploadify_tags.py └── uploadify_s3.py /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | .DS_Store 4 | *.pyc 5 | *.pyo 6 | .installed.cfg 7 | .idea/* 8 | bin/* 9 | develop-eggs/* 10 | dist/* 11 | downloads/* 12 | eggs/* 13 | parts/* 14 | src/*.egg-info 15 | db/* 16 | search_index/* 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Sam Charrington 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the author nor the names of other contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================================================================== 2 | Django Uploadify-S3 (DUS3): Browser-Based Uploads to Amazon S3 Made Easy 3 | ======================================================================== 4 | 5 | *Copyright (c) 2010, Sam Charrington (@samcharrington), http://geekfactor.charrington.com* 6 | 7 | Overview 8 | -------- 9 | 10 | This application aims to make it easy for you to add browser-based 11 | uploads to Amazon S3 to your Django projects using the Uploadify 12 | jQuery plugin. 13 | 14 | DUS3 is configuration driven, meaning you don't need to add any 15 | Uploadify or S3-specific code to your project to use these tools. 16 | 17 | For a working demo application based on DUS3, please visit: 18 | https://github.com/sbc/django-uploadify-s3-example 19 | 20 | Requirements 21 | ------------ 22 | 23 | Download the Uploadify ZIP file and unzip it in your project. This 24 | application assumes the files will be located in an ``uploadify`` 25 | directory within your MEDIA_URL directory. 26 | 27 | Note that Uploadify requires jQuery, and django-uploadify-s3 does not 28 | automatically include it. (To avoid conflicts.) You will need to 29 | include jQuery in your base template, or on the uploadify upload page. 30 | 31 | Background 32 | ---------- 33 | 34 | A general understanding of Uploadify and submitting browser-based 35 | uploads to Amazon S3 is helpful if not necessary: 36 | 37 | - Uploadify: 38 | http://www.uploadify.com/ 39 | 40 | - Browser-Based Uploads to Amazon S3: 41 | http://docs.amazonwebservices.com/AmazonS3/latest/dev/UsingHTTPPOST.html 42 | 43 | 44 | Installation & Use 45 | ------------------ 46 | 47 | 1. Add ``uploadify_s3`` to your ``INSTALLED_APPLICATIONS``. 48 | 49 | 2. Add any desired project-wide settings to your ``settings.py`` 50 | or equivalent. See Settings below. 51 | 52 | 3. Create a view and a template for your file upload page. 53 | 54 | 4. Call:: 55 | 56 | uploadify_s3.UploadifyS3().get_options_json() 57 | 58 | to generate the Uploadify configuration options and make this context 59 | available to your template. 60 | 61 | ``UploadifyS3()`` takes three optional parameters: 62 | 63 | - ``uploadify_options``: A dictionary of Uploadify options. 64 | - ``post_data``: A dictionary of POST variables that is used to 65 | set the Uploadify scriptData option and eventually sent to AWS. 66 | - ``conditions``: See conditions, below. 67 | 68 | These parameters are optional because DUS3 can in many cases pull 69 | all of its configuration data from your settings.py. The parameters 70 | override any values found in your settings file. 71 | 72 | 5. Load the DUS3 template tags and insert them in your template. 73 | 74 | Add this above any of the other tags to load the tag set:: 75 | {% load uploadify_tags %} 76 | 77 | Add this in your ```` to include the Uploadify jQuery, CSS and 78 | SWF files:: 79 | {% uploadify_head %} 80 | 81 | Add this in your ```` to include the Uploadify file upload 82 | widget. Initially this will appear as a "Select Files" button; 83 | when files are added the queue and progress bars will be 84 | displayed. The ``uploadify_options`` parameter is the options 85 | string you created in your view:: 86 | {% uploadify_widget uploadify_options %} 87 | 88 | Add this to your ```` to insert an Upload link/button. It takes a 89 | string parameter that allows you to add custom CSS classes:: 90 | {% uploadify_upload "extra css classes" %} 91 | 92 | 93 | Settings 94 | -------- 95 | 96 | DUS3 looks for an ``UPLOADIFY_DEFAULT_OPTIONS``, which is used to override 97 | Uploadify default option values on a project-wide bases. 98 | ``UPLOADIFY_DEFAULT_OPTIONS`` is a Python dictionary with keys corresponding 99 | to Uploadify options and native Python values, i.e. use Python boolean 100 | False to set a boolean option false, as opposed to string value of "false". 101 | These values may be overridden by passing options to ``UploadifyS3()`` 102 | in your view. 103 | 104 | In addition, DUS3 recognizes the following AWS S3 options: 105 | 106 | =========================== ================================================== 107 | ``AWS_ACCESS_KEY_ID`` Required. Either set in settings or pass in. 108 | ``AWS_SECRET_ACCESS_KEY`` Required. Either set in settings or pass in. 109 | ``AWS_BUCKET_NAME`` Required. Either set in settings or pass in. 110 | ``AWS_S3_SECURE_URLS`` Set to False to force http instead of https. 111 | ``AWS_BUCKET_URL`` Shouldn't need. Default is calculated from bucket name. 112 | ``AWS_DEFAULT_ACL`` Default is ``private``. 113 | ``AWS_DEFAULT_KEY_PATTERN`` The S3 ``key`` param. Default is ``'${filename}'``. 114 | ``AWS_DEFAULT_FORM_LIFETIME`` Signed form expiration time in secs from now. 115 | =========================== ================================================== 116 | 117 | Conditions 118 | ---------- 119 | 120 | To allow web browsers to post files to your S3 bucket you create and 121 | a policy document that describes the conditions under which AWS should 122 | accept a POST request. That policy document, and a signed version of it, 123 | is then included in the POST data. 124 | 125 | AWS first verifies the integrity of the policy document and then compares 126 | the conditions specified in the policy document with the POST data received. 127 | 128 | See: http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?AccessPolicyLanguage_UseCases_s3_a.html 129 | 130 | ``UploadifyS3()`` expects to receive a dictionary of conditions mapping a 131 | field name to a value object. Conditions are described by using different 132 | data types for the value object*: 133 | 134 | =============== ====================================================== 135 | Value Data Type Condition Applied 136 | =============== ====================================================== 137 | ``nil`` A starts-with test that will accept any value 138 | ``str`` An equality test using the given string 139 | ``list`` An equality test, against a value composed of all 140 | the array's items combined into a comma-delimited 141 | string 142 | ``dict`` An operation named by the ``op`` mapping, with a value 143 | given as the ``value`` mapping 144 | ``slice`` A range test, where the range must lie between the 145 | start and stop values of the slice object provided 146 | =============== ====================================================== 147 | 148 | *The semantics of the conditions array were very much inspired by 149 | James Murty's *Programming Amazon Web Services*. 150 | 151 | 152 | Troubleshooting 153 | --------------- 154 | 155 | 1. In order for the browser to communicate to your S3 bucket, you must 156 | upload a ``crossdomain.xml`` file to the root of your bucket. This example 157 | allows any browsers to communicate with your S3 bucket:: 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 2. Because Uploadify uses a Adobe Flash component to perform the actual 166 | upload, browser-based HTTP debugging tools like Firebug cannot see 167 | the traffic between the browser and S3. You can however use a network 168 | sniffer like Wireshark (http://www.wireshark.org) to view the traffic. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | version = '0.1' 4 | 5 | setup(name='django-uploadify-s3', 6 | version=version, 7 | description='A Django application for enabling browser-based uploads to Amazon S3 with the Uploadify uploader.', 8 | author='Sam Charrington', 9 | author_email='sbc@charrington.com', 10 | url='https://github.com/sbc/django-uploadify-s3', 11 | packages=['uploadify_s3', 'uploadify_s3.templatetags'], 12 | package_data = {'uploadify_s3': ['templates/*.html']}, 13 | classifiers=['Development Status :: 4 - Beta', 14 | 'Environment :: Web Environment', 15 | 'Framework :: Django', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: BSD License', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python', 20 | 'Topic :: Utilities'], 21 | ) -------------------------------------------------------------------------------- /uploadify_s3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbc/django-uploadify-s3/39aae4350ae60633cf2b270e9de69e5483d31e03/uploadify_s3/__init__.py -------------------------------------------------------------------------------- /uploadify_s3/templates/uploadify_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /uploadify_s3/templates/uploadify_upload.html: -------------------------------------------------------------------------------- 1 | Upload -------------------------------------------------------------------------------- /uploadify_s3/templates/uploadify_widget.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /uploadify_s3/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbc/django-uploadify-s3/39aae4350ae60633cf2b270e9de69e5483d31e03/uploadify_s3/templatetags/__init__.py -------------------------------------------------------------------------------- /uploadify_s3/templatetags/uploadify_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | @register.inclusion_tag('uploadify_head.html') 7 | def uploadify_head(): 8 | return { 9 | 'MEDIA_URL': settings.MEDIA_URL, 10 | } 11 | 12 | @register.inclusion_tag('uploadify_widget.html') 13 | def uploadify_widget(options): 14 | return { 15 | 'uploadify_options': options, 16 | } 17 | 18 | @register.inclusion_tag('uploadify_upload.html') 19 | def uploadify_upload(css_classes=""): 20 | return { 21 | 'css_classes': css_classes, 22 | } -------------------------------------------------------------------------------- /uploadify_s3/uploadify_s3.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from urllib import quote_plus 4 | from datetime import datetime 5 | from datetime import timedelta 6 | import base64 7 | import hmac 8 | import hashlib 9 | import json 10 | 11 | UPLOADIFY_OPTIONS = ('auto', 'buttonImg', 'buttonText', 'cancelImg', 'checkScript', 'displayData', 'expressInstall', 'fileDataName', 'fileDesc', 'fileExt', 'folder', 'height', 'hideButton', 'method', 'multi', 'queueID', 'queueSizeLimit', 'removeCompleted', 'rollover', 'script','scriptAccess', 'scriptData', 'simUploadLimit', 'sizeLimit', 'uploader', 'width', 'wmode') 12 | 13 | UPLOADIFY_METHODS = ('onAllComplete', 'onCancel', 'onCheck', 'onClearQueue', 'onComplete', 'onError', 'onInit', 'onOpen', 'onProgress', 'onQueueFull', 'onSelect', 'onSelectOnce', 'onSWFReady') 14 | 15 | PASS_THRU_OPTIONS = ('folder', 'fileExt',) 16 | FILTERED_KEYS = ('filename',) 17 | EXCLUDED_KEYS = ('AWSAccessKeyId', 'policy', 'signature') 18 | 19 | # AWS Options 20 | ACCESS_KEY_ID = getattr(settings, 'AWS_ACCESS_KEY_ID', None) 21 | SECRET_ACCESS_KEY = getattr(settings, 'AWS_SECRET_ACCESS_KEY', None) 22 | BUCKET_NAME = getattr(settings, 'AWS_BUCKET_NAME', None) 23 | SECURE_URLS = getattr(settings, 'AWS_S3_SECURE_URLS', True) 24 | BUCKET_URL = getattr(settings, 'AWS_BUCKET_URL', 'http://' if SECURE_URLS else 'https://' + BUCKET_NAME + '.s3.amazonaws.com') 25 | DEFAULT_ACL = getattr(settings, 'AWS_DEFAULT_ACL', 'private') 26 | DEFAULT_KEY_PATTERN = getattr(settings, 'AWS_DEFAULT_KEY_PATTERN', '${filename}') 27 | DEFAULT_FORM_TIME = getattr(settings, 'AWS_DEFAULT_FORM_LIFETIME', 36000) # 10 HOURS 28 | 29 | # Defaults for required Uploadify options 30 | DEFAULT_CANCELIMG = settings.MEDIA_URL + "uploadify/cancel.png" 31 | DEFAULT_UPLOADER = settings.MEDIA_URL + "uploadify/uploadify.swf" 32 | 33 | class UploadifyS3(object): 34 | """Uploadify for Amazon S3""" 35 | 36 | def __init__(self, uploadify_options={}, post_data={}, conditions={}): 37 | self.options = getattr(settings, 'UPLOADIFY_DEFAULT_OPTIONS', {}) 38 | self.options.update(uploadify_options) 39 | 40 | if any(True for key in self.options if key not in UPLOADIFY_OPTIONS + UPLOADIFY_METHODS): 41 | raise ImproperlyConfigured("Attempted to initialize with unrecognized option '%s'." % key) 42 | 43 | _set_default_if_none(self.options, 'cancelImg', DEFAULT_CANCELIMG) 44 | _set_default_if_none(self.options, 'uploader', DEFAULT_UPLOADER) 45 | _set_default_if_none(self.options, 'script', BUCKET_URL) 46 | 47 | self.post_data = post_data 48 | 49 | _set_default_if_none(self.post_data, 'key', _uri_encode(DEFAULT_KEY_PATTERN)) 50 | _set_default_if_none(self.post_data, 'acl', DEFAULT_ACL) 51 | 52 | try: 53 | _set_default_if_none(self.post_data, 'bucket', BUCKET_NAME) 54 | except ValueError: 55 | raise ImproperlyConfigured("Bucket name is a required property.") 56 | 57 | try: 58 | _set_default_if_none(self.post_data, 'AWSAccessKeyId', _uri_encode(ACCESS_KEY_ID)) 59 | except ValueError: 60 | raise ImproperlyConfigured("AWS Access Key ID is a required property.") 61 | 62 | self.conditions = build_conditions(self.options, self.post_data, conditions) 63 | 64 | if not SECRET_ACCESS_KEY: 65 | raise ImproperlyConfigured("AWS Secret Access Key is a required property.") 66 | 67 | expiration_time = datetime.utcnow() + timedelta(seconds=DEFAULT_FORM_TIME) 68 | self.policy_string = build_post_policy(expiration_time, self.conditions) 69 | 70 | self.policy = base64.b64encode(self.policy_string) 71 | 72 | self.signature = base64.encodestring(hmac.new(SECRET_ACCESS_KEY, self.policy, hashlib.sha1).digest()).strip() 73 | 74 | self.post_data['policy'] = self.policy 75 | self.post_data['signature'] = _uri_encode(self.signature) 76 | self.options['scriptData'] = self.post_data 77 | # self.options['policyDebug'] = self.policy_string 78 | 79 | def get_options_json(self): 80 | # return json.dumps(self.options) 81 | 82 | subs = [] 83 | for key in self.options: 84 | if key in UPLOADIFY_METHODS: 85 | subs.append(('"%%%s%%"' % key, self.options[key])) 86 | self.options[key] = "%%%s%%" % key 87 | 88 | out = json.dumps(self.options) 89 | 90 | for search, replace in subs: 91 | out = out.replace(search, replace) 92 | 93 | return out 94 | 95 | def build_conditions(options, post_data, conditions): 96 | # PASS_THRU_OPTIONS are Uploadify options that if set in the settings are 97 | # passed into the POST. As a result, a default policy condition is created here. 98 | for opt in PASS_THRU_OPTIONS: 99 | if opt in options and opt not in conditions: 100 | conditions[opt] = None 101 | 102 | # FILTERED_KEYS are those created by Uploadify and passed into the POST on submit. 103 | # As a result, a default policy condition is created here. 104 | for opt in FILTERED_KEYS: 105 | if opt not in conditions: 106 | conditions[opt] = None 107 | 108 | conds = post_data.copy() 109 | conds.update(conditions) 110 | 111 | # EXCLUDED_KEYS are those that are set by UploadifyS3 but need to be stripped out 112 | # for the purposes of creating conditions. 113 | for key in EXCLUDED_KEYS: 114 | if key in conds: 115 | del conds[key] 116 | 117 | return conds 118 | 119 | def build_post_policy(expiration_time, conditions): 120 | """ Function to build S3 POST policy. Adapted from Programming Amazon Web Services, Murty, pg 104-105. """ 121 | conds = [] 122 | for name, test in conditions.iteritems(): 123 | if test is None: 124 | # A None condition value means allow anything. 125 | conds.append('["starts-with", "$%s", ""]' % name) 126 | elif isinstance(test,str) or isinstance(test, unicode): 127 | conds.append('{"%s": "%s" }' % (name, test)) 128 | elif isinstance(test,list): 129 | conds.append('{"%s": "%s" }' % (name, ','.join(test))) 130 | elif isinstance(test, dict): 131 | operation = test['op'] 132 | value = test['value'] 133 | conds.append('["%s", "$%s", "%s"]' % (operation, name, value)) 134 | elif isinstance(test,slice): 135 | conds.append('["%s", "%s", "%s"]' %(name, test.start, test.stop)) 136 | else: 137 | raise TypeError("Unexpected value type for condition '%s': %s" % (name, type(test))) 138 | 139 | return '{"expiration": "%s", "conditions": [%s]}' \ 140 | % (expiration_time.strftime("%Y-%m-%dT%H:%M:%SZ"), ', '.join(conds)) 141 | 142 | def _uri_encode(str): 143 | try: 144 | # The Uploadify flash component apparently decodes the scriptData once, so we need to encode twice here. 145 | return quote_plus(quote_plus(str, safe='~'), safe='~') 146 | except: 147 | raise ValueError 148 | 149 | def _set_default_if_none(dict, key, default=None): 150 | if key not in dict: 151 | if default: 152 | dict[key] = default 153 | else: 154 | raise ValueError 155 | 156 | 157 | --------------------------------------------------------------------------------