├── s3direct ├── models.py ├── __init__.py ├── urls.py ├── static │ └── s3direct │ │ ├── css │ │ ├── styles.css │ │ └── bootstrap-progress.min.css │ │ └── js │ │ └── scripts.js ├── fields.py ├── widgets.py ├── views.py ├── utils.py └── tests.py ├── example ├── cat │ ├── __init__.py │ ├── tests.py │ ├── urls.py │ ├── forms.py │ ├── views.py │ ├── admin.py │ └── models.py ├── example │ ├── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py ├── s3direct ├── templates │ └── form.html └── manage.py ├── MANIFEST.in ├── screenshot.png ├── .gitignore ├── .drone.yml ├── .travis.yml ├── setup.py ├── LICENCE ├── runtests.py └── README.md /s3direct/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/cat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /s3direct/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/s3direct: -------------------------------------------------------------------------------- 1 | ../s3direct -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include s3direct * -------------------------------------------------------------------------------- /example/cat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/django-s3direct/master/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | dist/* 4 | *.egg-info 5 | venv 6 | example/db.sqlite3 7 | /build -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | image: bradleyg/python 2 | 3 | script: 4 | - pip install -q Django==1.8 5 | - python runtests.py 6 | -------------------------------------------------------------------------------- /example/cat/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from .views import MyView 4 | 5 | 6 | urlpatterns = patterns('', 7 | url('', MyView.as_view(), name='form'), 8 | ) -------------------------------------------------------------------------------- /example/cat/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from s3direct.widgets import S3DirectWidget 4 | 5 | 6 | class S3DirectUploadForm(forms.Form): 7 | images = forms.URLField(widget=S3DirectWidget(dest='imgs')) -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | env: 6 | - DJANGO=1.5 7 | - DJANGO=1.6 8 | - DJANGO=1.7 9 | install: 10 | - pip install -q Django==$DJANGO 11 | script: 12 | - python runtests.py 13 | -------------------------------------------------------------------------------- /example/cat/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic import FormView 3 | 4 | from .forms import S3DirectUploadForm 5 | 6 | 7 | class MyView(FormView): 8 | template_name = 'form.html' 9 | form_class = S3DirectUploadForm -------------------------------------------------------------------------------- /example/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | s3direct 5 | {{ form.media }} 6 | 7 | 8 |
{% csrf_token %} 9 | {{ form.as_p }} 10 |
11 | 12 | -------------------------------------------------------------------------------- /s3direct/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from s3direct.views import get_upload_params 3 | 4 | urlpatterns = patterns('', 5 | url('^get_upload_params/', 6 | get_upload_params, name='s3direct'), 7 | ) 8 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/cat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Cat, Kitten 4 | 5 | 6 | class KittenAdminInline(admin.StackedInline): 7 | model = Kitten 8 | extra = 1 9 | 10 | 11 | class CatAdmin(admin.ModelAdmin): 12 | inlines = [KittenAdminInline, ] 13 | 14 | 15 | admin.site.register(Cat, CatAdmin) 16 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | 5 | admin.autodiscover() 6 | 7 | 8 | urlpatterns = patterns('', 9 | url(r'^admin/', include(admin.site.urls)), 10 | url(r'^s3direct/', include('s3direct.urls')), 11 | url(r'^form/', include('cat.urls')), 12 | ) -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sample_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /example/cat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from s3direct.fields import S3DirectField 3 | 4 | 5 | class Cat(models.Model): 6 | video = S3DirectField(dest='custom_filename', blank=True) 7 | 8 | def __unicode__(self): 9 | return str(self.video) 10 | 11 | 12 | class Kitten(models.Model): 13 | mother = models.ForeignKey('Cat') 14 | 15 | video = S3DirectField(dest='vids', blank=True) 16 | image = S3DirectField(dest='imgs', blank=True) 17 | pdf = S3DirectField(dest='files', blank=True) 18 | 19 | def __unicode__(self): 20 | return str(self.video) 21 | -------------------------------------------------------------------------------- /s3direct/static/s3direct/css/styles.css: -------------------------------------------------------------------------------- 1 | .s3direct { 2 | float: left; 3 | } 4 | 5 | .s3direct .progress { 6 | background: #333; 7 | width: 200px; 8 | } 9 | 10 | .s3direct .file-remove { 11 | margin-top: 10px; 12 | text-transform: uppercase; 13 | } 14 | 15 | .s3direct .progress, 16 | .s3direct .file-link, 17 | .s3direct .file-remove, 18 | .s3direct .file-input { 19 | display: none; 20 | float: left; 21 | clear: left; 22 | } 23 | 24 | .s3direct.progress-active .progress, 25 | .s3direct.link-active .file-link, 26 | .s3direct.link-active .file-remove, 27 | .s3direct.form-active .file-input { 28 | display: block; 29 | } -------------------------------------------------------------------------------- /s3direct/fields.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Field 2 | from django.conf import settings 3 | from s3direct.widgets import S3DirectWidget 4 | 5 | 6 | class S3DirectField(Field): 7 | def __init__(self, *args, **kwargs): 8 | dest = kwargs.pop('dest', None) 9 | self.widget = S3DirectWidget(dest=dest) 10 | super(S3DirectField, self).__init__(*args, **kwargs) 11 | 12 | def get_internal_type(self): 13 | return 'TextField' 14 | 15 | def formfield(self, *args, **kwargs): 16 | kwargs['widget'] = self.widget 17 | return super(S3DirectField, self).formfield(*args, **kwargs) 18 | 19 | 20 | if 'south' in settings.INSTALLED_APPS: 21 | from south.modelsinspector import add_introspection_rules 22 | add_introspection_rules([], ["^s3direct\.fields\.S3DirectField"]) 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | f = open(os.path.join(os.path.dirname(__file__), 'README.md')) 5 | readme = f.read() 6 | f.close() 7 | 8 | setup( 9 | name='django-s3direct', 10 | version='0.3.11', 11 | description='Add direct uploads to S3 functionality with a progress bar to file input fields.', 12 | long_description=readme, 13 | author="Bradley Griffiths", 14 | author_email='bradley.griffiths@gmail.com', 15 | url='https://github.com/bradleyg/django-s3direct', 16 | packages=['s3direct'], 17 | include_package_data=True, 18 | install_requires=['django>=1.6.2'], 19 | zip_safe=False, 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Environment :: Web Environment', 23 | 'Framework :: Django', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python :: 2.7', 28 | 'Programming Language :: Python :: 3.4', 29 | ], 30 | ) -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Bradley Griffiths 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /s3direct/widgets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.forms import widgets 4 | from django.utils.safestring import mark_safe 5 | from django.core.urlresolvers import reverse 6 | from django.conf import settings 7 | 8 | 9 | class S3DirectWidget(widgets.TextInput): 10 | 11 | html = ( 12 | '
' 13 | ' {file_name}' 14 | ' Remove' 15 | ' ' 16 | ' ' 17 | ' ' 18 | '
' 19 | '
' 20 | '
' 21 | '
' 22 | ) 23 | 24 | class Media: 25 | js = ( 26 | 's3direct/js/scripts.js', 27 | ) 28 | css = { 29 | 'all': ( 30 | 's3direct/css/bootstrap-progress.min.css', 31 | 's3direct/css/styles.css', 32 | ) 33 | } 34 | 35 | def __init__(self, *args, **kwargs): 36 | self.dest = kwargs.pop('dest', None) 37 | super(S3DirectWidget, self).__init__(*args, **kwargs) 38 | 39 | def render(self, name, value, attrs=None): 40 | output = self.html.format( 41 | policy_url=reverse('s3direct'), 42 | element_id=self.build_attrs(attrs).get('id'), 43 | file_name=os.path.basename(value or ''), 44 | dest=self.dest, 45 | file_url=value or '', 46 | name=name) 47 | 48 | return mark_safe(output) 49 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from os import environ 4 | 5 | import django 6 | from django.conf import settings 7 | 8 | settings.configure(DEBUG=True, 9 | DATABASES={ 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | } 13 | }, 14 | ROOT_URLCONF='s3direct.urls', 15 | INSTALLED_APPS=('django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.admin', 19 | 's3direct',), 20 | MIDDLEWARE_CLASSES=('django.contrib.sessions.middleware.SessionMiddleware', 21 | 'django.contrib.auth.middleware.AuthenticationMiddleware',), 22 | AWS_ACCESS_KEY_ID=environ.get('AWS_ACCESS_KEY_ID', ''), 23 | AWS_SECRET_ACCESS_KEY=environ.get( 24 | 'AWS_SECRET_ACCESS_KEY', ''), 25 | AWS_STORAGE_BUCKET_NAME=environ.get( 26 | 'AWS_VIDEO_STORAGE_BUCKET_NAME', 27 | 'test-bucket'), 28 | S3DIRECT_REGION='us-east-1', 29 | S3DIRECT_DESTINATIONS={ 30 | 'misc': (lambda original_filename: 'images/unique.jpg',), 31 | 'files': ('uploads/files', lambda u: u.is_staff,), 32 | 'imgs': ('uploads/imgs', lambda u: True, ['image/jpeg', 'image/png'],), 33 | 'vids': ('uploads/vids', lambda u: u.is_authenticated(), ['video/mp4'],) 34 | }) 35 | 36 | if hasattr(django, 'setup'): 37 | django.setup() 38 | 39 | if django.get_version() < '1.6': 40 | from django.test.simple import DjangoTestSuiteRunner 41 | test_runner = DjangoTestSuiteRunner(verbosity=1) 42 | else: 43 | from django.test.runner import DiscoverRunner 44 | test_runner = DiscoverRunner(verbosity=1) 45 | 46 | failures = test_runner.run_tests(['s3direct', ]) 47 | 48 | if failures: 49 | sys.exit(failures) 50 | -------------------------------------------------------------------------------- /s3direct/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import HttpResponse 4 | from django.views.decorators.http import require_POST 5 | from django.conf import settings 6 | 7 | from .utils import create_upload_data, get_at 8 | 9 | import calendar, time 10 | 11 | DESTINATIONS = getattr(settings, 'S3DIRECT_DESTINATIONS', None) 12 | 13 | 14 | @require_POST 15 | def get_upload_params(request): 16 | content_type = request.POST['type'] 17 | filename = request.POST['name'] 18 | 19 | dest = DESTINATIONS.get(request.POST['dest']) 20 | 21 | if not dest: 22 | data = json.dumps({'error': 'File destination does not exist.'}) 23 | return HttpResponse(data, content_type="application/json", status=400) 24 | 25 | key = get_at(0, dest) 26 | auth = get_at(1, dest) 27 | allowed = get_at(2, dest) 28 | acl = get_at(3, dest) 29 | bucket = get_at(4, dest) 30 | cache_control = get_at(5, dest) 31 | content_disposition = get_at(6, dest) 32 | 33 | if not acl: 34 | acl = 'public-read' 35 | 36 | if not key: 37 | data = json.dumps({'error': 'Missing destination path.'}) 38 | return HttpResponse(data, content_type="application/json", status=403) 39 | 40 | if auth and not auth(request.user): 41 | data = json.dumps({'error': 'Permission denied.'}) 42 | return HttpResponse(data, content_type="application/json", status=403) 43 | 44 | if (allowed and content_type not in allowed) and allowed != '*': 45 | data = json.dumps({'error': 'Invalid file type (%s).' % content_type}) 46 | return HttpResponse(data, content_type="application/json", status=400) 47 | 48 | if hasattr(key, '__call__'): 49 | key = key(filename) 50 | else: 51 | # The literal string '${filename}' is an S3 field variable for key. 52 | # https://aws.amazon.com/articles/1434#aws-table 53 | key = '%s/${filename}' % key 54 | 55 | # Adds timestamp to filename to prevent duplicate files in S3. 56 | keyname, ext = key.split('.') 57 | key = keyname + str(calendar.timegm(time.gmtime())) + "." + ext 58 | data = create_upload_data(content_type, key, acl, bucket, cache_control, content_disposition) 59 | 60 | return HttpResponse(json.dumps(data), content_type="application/json") 61 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 3 | 4 | SECRET_KEY = 'd0au$i5he(#ais5@-i@rv=963$a@4d2p2fmnc7(gyc2ecoi^_)' 5 | 6 | DEBUG = True 7 | TEMPLATE_DEBUG = DEBUG 8 | 9 | ALLOWED_HOSTS = [] 10 | 11 | INSTALLED_APPS = ( 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 's3direct', 19 | 'cat', 20 | ) 21 | 22 | MIDDLEWARE_CLASSES = ( 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.middleware.csrf.CsrfViewMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | ) 30 | 31 | ROOT_URLCONF = 'example.urls' 32 | WSGI_APPLICATION = 'example.wsgi.application' 33 | 34 | DATABASES = { 35 | 'default': { 36 | 'ENGINE': 'django.db.backends.sqlite3', 37 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 38 | } 39 | } 40 | 41 | LANGUAGE_CODE = 'en-us' 42 | TIME_ZONE = 'UTC' 43 | USE_I18N = True 44 | USE_L10N = True 45 | USE_TZ = True 46 | 47 | LOGGING = { 48 | 'version': 1, 49 | 'disable_existing_loggers': False, 50 | 'handlers': { 51 | 'console': { 52 | 'class': 'logging.StreamHandler', 53 | }, 54 | }, 55 | 'loggers': { 56 | 'django.request': { 57 | 'handlers': ['console'], 58 | 'level': 'INFO', 59 | 'propagate': True, 60 | }, 61 | } 62 | } 63 | 64 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 65 | STATIC_URL = '/static/' 66 | 67 | TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates'),) 68 | 69 | AWS_ACCESS_KEY_ID = '' 70 | AWS_SECRET_ACCESS_KEY = '' 71 | AWS_STORAGE_BUCKET_NAME = '' 72 | S3DIRECT_REGION = 'us-east-1' 73 | 74 | 75 | def create_filename(filename): 76 | import uuid 77 | ext = filename.split('.')[-1] 78 | filename = '%s.%s' % (uuid.uuid4().hex, ext) 79 | return os.path.join('images', filename) 80 | 81 | 82 | S3DIRECT_DESTINATIONS={ 83 | # Allow anybody to upload any MIME type 84 | 'misc': ('uploads/misc',), 85 | 86 | # Allow staff users to upload any MIME type 87 | 'files': ('uploads/files', lambda u: u.is_staff,), 88 | 89 | # Allow anybody to upload jpeg's and png's. 90 | 'imgs': ('uploads/imgs', lambda u: True, ['image/jpeg', 'image/png'],), 91 | 92 | # Allow authenticated users to upload mp4's 93 | 'vids': ('uploads/vids', lambda u: u.is_authenticated(), ['video/mp4'],), 94 | 95 | # Allow anybody to upload any MIME type with a custom name function 96 | 'custom_filename': (create_filename,), 97 | } 98 | -------------------------------------------------------------------------------- /s3direct/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | from datetime import datetime, timedelta 5 | from base64 import b64encode 6 | 7 | from django.conf import settings 8 | 9 | 10 | REGIONS = { 11 | 'us-east-1': 's3.amazonaws.com', 12 | 'us-west-2': 's3-us-west-2.amazonaws.com', 13 | 'us-west-1': 's3-us-west-1.amazonaws.com', 14 | 'eu-west-1': 's3-eu-west-1.amazonaws.com', 15 | 'eu-central-1': 's3.eu-central-1.amazonaws.com', 16 | 'ap-southeast-1': 's3-ap-southeast-1.amazonaws.com', 17 | 'ap-southeast-2': 's3-ap-southeast-2.amazonaws.com', 18 | 'ap-northeast-1': 's3-ap-northeast-1.amazonaws.com', 19 | 'sa-east-1': 's3-sa-east-1.amazonaws.com', 20 | } 21 | 22 | 23 | def get_at(index, t): 24 | try: 25 | value = t[index] 26 | except IndexError: 27 | value = None 28 | return value 29 | 30 | 31 | def create_upload_data(content_type, key, acl, bucket=None, cache_control=None, content_disposition=None): 32 | access_key = settings.AWS_ACCESS_KEY_ID 33 | secret_access_key = settings.AWS_SECRET_ACCESS_KEY 34 | bucket = bucket or settings.AWS_VIDEO_STORAGE_BUCKET_NAME 35 | region = getattr(settings, 'S3DIRECT_REGION', None) 36 | endpoint = REGIONS.get(region, 's3.amazonaws.com') 37 | 38 | expires_in = datetime.utcnow() + timedelta(seconds=60*1000) 39 | expires = expires_in.strftime('%Y-%m-%dT%H:%M:%S.000Z') 40 | 41 | policy_dict = { 42 | "expiration": expires, 43 | "conditions": [ 44 | {"bucket": bucket}, 45 | {"acl": acl}, 46 | {"Content-Type": content_type}, 47 | ["starts-with", "$key", ""], 48 | {"success_action_status": "201"} 49 | ] 50 | } 51 | 52 | if cache_control: 53 | policy_dict['conditions'].append({'Cache-Control': cache_control}) 54 | 55 | if content_disposition: 56 | policy_dict['conditions'].append({'Content-Disposition': content_disposition}) 57 | 58 | policy_object = json.dumps(policy_dict) 59 | 60 | policy = b64encode( 61 | policy_object.replace('\n', '').replace('\r', '').encode()) 62 | 63 | signature = hmac.new( 64 | secret_access_key.encode(), policy, hashlib.sha1).digest() 65 | 66 | signature_b64 = b64encode(signature) 67 | 68 | structure = getattr(settings, 'S3DIRECT_URL_STRUCTURE', 'https://{1}.{0}') 69 | bucket_url = structure.format(endpoint, bucket) 70 | 71 | return_dict = { 72 | "policy": policy.decode(), 73 | "signature": signature_b64.decode(), 74 | "key": key, 75 | "AWSAccessKeyId": access_key, 76 | "form_action": bucket_url, 77 | "success_action_status": "201", 78 | "acl": acl, 79 | "Content-Type": content_type 80 | } 81 | 82 | if cache_control: 83 | return_dict['Cache-Control'] = cache_control 84 | 85 | if content_disposition: 86 | return_dict['Content-Disposition'] = content_disposition 87 | 88 | return return_dict 89 | -------------------------------------------------------------------------------- /s3direct/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test.utils import override_settings 4 | from django.contrib.auth.models import User 5 | from django.core.urlresolvers import reverse, resolve 6 | from django.test import TestCase 7 | 8 | from s3direct import widgets 9 | 10 | 11 | HTML_OUTPUT = ( 12 | '
' 13 | ' ' 14 | ' Remove' 15 | ' ' 16 | ' ' 17 | ' ' 18 | '
' 19 | '
' 20 | '
' 21 | '
' 22 | ) 23 | 24 | FOO_RESPONSE = { 25 | u'AWSAccessKeyId': u'', 26 | u'form_action': u'https://s3.amazonaws.com/test-bucket', 27 | u'success_action_status': u'201', 28 | u'acl': u'public-read', 29 | u'key': u'uploads/imgs/${filename}', 30 | u'Content-Type': u'image/jpeg' 31 | } 32 | 33 | 34 | class WidgetTest(TestCase): 35 | def setUp(self): 36 | admin = User.objects.create_superuser('admin', 'u@email.com', 'admin') 37 | admin.save() 38 | 39 | def test_urls(self): 40 | reversed_url = reverse('s3direct') 41 | resolved_url = resolve('/get_upload_params/') 42 | self.assertEqual(reversed_url, '/get_upload_params/') 43 | self.assertEqual(resolved_url.view_name, 's3direct') 44 | 45 | def test_widget_html(self): 46 | widget = widgets.S3DirectWidget(dest='foo') 47 | self.assertEqual(widget.render('filename', None), HTML_OUTPUT) 48 | 49 | def test_signing_logged_in(self): 50 | self.client.login(username='admin', password='admin') 51 | data = {'dest': 'files', 'name': 'image.jpg', 'type': 'image/jpeg'} 52 | response = self.client.post(reverse('s3direct'), data) 53 | self.assertEqual(response.status_code, 200) 54 | 55 | def test_signing_logged_out(self): 56 | data = {'dest': 'files', 'name': 'image.jpg', 'type': 'image/jpeg'} 57 | response = self.client.post(reverse('s3direct'), data) 58 | self.assertEqual(response.status_code, 403) 59 | 60 | def test_allowed_type(self): 61 | data = {'dest': 'imgs', 'name': 'image.jpg', 'type': 'image/jpeg'} 62 | response = self.client.post(reverse('s3direct'), data) 63 | self.assertEqual(response.status_code, 200) 64 | 65 | def test_disallowed_type(self): 66 | data = {'dest': 'imgs', 'name': 'image.mp4', 'type': 'video/mp4'} 67 | response = self.client.post(reverse('s3direct'), data) 68 | self.assertEqual(response.status_code, 400) 69 | 70 | def test_allowed_type_logged_in(self): 71 | self.client.login(username='admin', password='admin') 72 | data = {'dest': 'vids', 'name': 'video.mp4', 'type': 'video/mp4'} 73 | response = self.client.post(reverse('s3direct'), data) 74 | self.assertEqual(response.status_code, 200) 75 | 76 | def test_disallowed_type_logged_out(self): 77 | data = {'dest': 'vids', 'name': 'video.mp4', 'type': 'video/mp4'} 78 | response = self.client.post(reverse('s3direct'), data) 79 | self.assertEqual(response.status_code, 403) 80 | 81 | def test_signing_fields(self): 82 | self.client.login(username='admin', password='admin') 83 | data = {'dest': 'imgs', 'name': 'image.jpg', 'type': 'image/jpeg'} 84 | response = self.client.post(reverse('s3direct'), data) 85 | response_dict = json.loads(response.content.decode()) 86 | self.assertTrue(u'signature' in response_dict) 87 | self.assertTrue(u'policy' in response_dict) 88 | self.assertDictContainsSubset(FOO_RESPONSE, response_dict) 89 | 90 | def test_signing_fields_unique_filename(self): 91 | data = {'dest': 'misc', 'name': 'image.jpg', 'type': 'image/jpeg'} 92 | response = self.client.post(reverse('s3direct'), data) 93 | response_dict = json.loads(response.content.decode()) 94 | self.assertTrue(u'signature' in response_dict) 95 | self.assertTrue(u'policy' in response_dict) 96 | FOO_RESPONSE['key'] = 'images/unique.jpg' 97 | self.assertDictContainsSubset(FOO_RESPONSE, response_dict) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-s3direct 2 | =============== 3 | 4 | Upload files direct to S3 from Django 5 | ------------------------------------- 6 | 7 | [![Build Status](https://travis-ci.org/bradleyg/django-s3direct.svg?branch=master)](https://travis-ci.org/bradleyg/django-s3direct) 8 | [![PyPi Version](https://pypip.in/v/django-s3direct/badge.png)](https://crate.io/packages/django-s3direct) 9 | [![PyPi Downloads](https://pypip.in/d/django-s3direct/badge.png)](https://crate.io/packages/django-s3direct) 10 | 11 | Add direct uploads to AWS S3 functionality with a progress bar to file input fields. 12 | 13 | ![screenshot](https://raw.githubusercontent.com/bradleyg/django-s3direct/master/screenshot.png) 14 | 15 | ## Support 16 | Python 2/3 17 | Chrome / Safari / Firefox / IE10+ 18 | 19 | For older browser support use version 0.1.10. 20 | 21 | ## Installation 22 | 23 | Install with Pip: 24 | 25 | ```pip install django-s3direct``` 26 | 27 | ## S3 Setup 28 | 29 | Setup a CORS policy on your S3 bucket. 30 | 31 | ```xml 32 | 33 | 34 | * 35 | PUT 36 | POST 37 | GET 38 | 3000 39 | * 40 | 41 | 42 | ``` 43 | 44 | ## Django Setup 45 | 46 | ### settings.py 47 | 48 | ```python 49 | INSTALLED_APPS = [ 50 | ... 51 | 's3direct', 52 | ... 53 | ] 54 | 55 | # AWS keys 56 | AWS_SECRET_ACCESS_KEY = '' 57 | AWS_ACCESS_KEY_ID = '' 58 | AWS_STORAGE_BUCKET_NAME = '' 59 | 60 | # The region of your bucket, more info: 61 | # http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region 62 | S3DIRECT_REGION = 'us-east-1' 63 | 64 | # Destinations in the following format: 65 | # {destination_key: (path_or_function, auth_test, [allowed_mime_types], permissions, custom_bucket)} 66 | # 67 | # 'destination_key' is the key to use for the 'dest' attribute on your widget or model field 68 | S3DIRECT_DESTINATIONS = { 69 | # Allow anybody to upload any MIME type 70 | 'misc': ('uploads/misc',), 71 | 72 | # Allow staff users to upload any MIME type 73 | 'files': ('uploads/files', lambda u: u.is_staff,), 74 | 75 | # Allow anybody to upload jpeg's and png's. 76 | 'imgs': ('uploads/imgs', lambda u: True, ['image/jpeg', 'image/png'],), 77 | 78 | # Allow authenticated users to upload mp4's 79 | 'vids': ('uploads/vids', lambda u: u.is_authenticated(), ['video/mp4'],), 80 | 81 | # Allow anybody to upload any MIME type with a custom name function, eg: 82 | 'custom_filename': (lambda original_filename: 'images/unique.jpg',), 83 | 84 | # Specify a non-default bucket for PDFs 85 | 'pdfs': ('/', lambda u: True, ['application/pdf'], None, 'pdf-bucket',), 86 | 87 | # Allow logged in users to upload any type of file and give it a private acl: 88 | 'private': ( 89 | 'uploads/vids', 90 | lambda u: u.is_authenticated(), 91 | '*', 92 | 'private') 93 | 94 | # Allow authenticated users to upload with cache-control for a month and content-disposition set to attachment 95 | 'cached': ( 96 | 'uploads/vids', 97 | lambda u: u.is_authenticated(), 98 | '*', 99 | 'public-read', 100 | AWS_STORAGE_BUCKET_NAME, 101 | 'max-age=2592000', 102 | 'attachment') 103 | } 104 | ``` 105 | 106 | ### urls.py 107 | 108 | ```python 109 | urlpatterns = patterns('', 110 | url(r'^s3direct/', include('s3direct.urls')), 111 | ) 112 | ``` 113 | 114 | Run ```python manage.py collectstatic``` if required. 115 | 116 | ## Use in Django admin only 117 | 118 | ### models.py 119 | 120 | ```python 121 | from django.db import models 122 | from s3direct.fields import S3DirectField 123 | 124 | class Example(models.Model): 125 | video = S3DirectField(dest='destination_key_from_settings') 126 | ``` 127 | 128 | ## Use the widget in a custom form 129 | 130 | ### forms.py 131 | 132 | ```python 133 | from django import forms 134 | from s3direct.widgets import S3DirectWidget 135 | 136 | class S3DirectUploadForm(forms.Form): 137 | images = forms.URLField(widget=S3DirectWidget(dest='destination_key_from_settings')) 138 | ``` 139 | 140 | ### views.py 141 | 142 | ```python 143 | from django.views.generic import FormView 144 | from .forms import S3DirectUploadForm 145 | 146 | class MyView(FormView): 147 | template_name = 'form.html' 148 | form_class = S3DirectUploadForm 149 | ``` 150 | 151 | ### templates/form.html 152 | 153 | ```html 154 | 155 | 156 | 157 | s3direct 158 | {{ form.media }} 159 | 160 | 161 |
{% csrf_token %} 162 | {{ form.as_p }} 163 |
164 | 165 | 166 | ``` 167 | 168 | ## Examples 169 | Examples of both approaches can be found in the examples folder. To run them: 170 | ```shell 171 | $ git clone git@github.com:bradleyg/django-s3direct.git 172 | $ cd django-s3direct 173 | $ python setup.py install 174 | $ cd example 175 | 176 | # Add your AWS keys to settings.py 177 | 178 | $ python manage.py syncdb 179 | $ python manage.py runserver 0.0.0.0:5000 180 | ``` 181 | 182 | Visit ```http://localhost:5000/admin``` to view the admin widget and ```http://localhost:5000/form``` to view the custom form widget. 183 | -------------------------------------------------------------------------------- /s3direct/static/s3direct/js/scripts.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | "use strict" 4 | 5 | var getCookie = function(name) { 6 | var value = '; ' + document.cookie, 7 | parts = value.split('; ' + name + '=') 8 | if (parts.length == 2) return parts.pop().split(';').shift() 9 | } 10 | 11 | var request = function(method, url, data, headers, el, showProgress, cb) { 12 | var req = new XMLHttpRequest() 13 | req.open(method, url, true) 14 | 15 | Object.keys(headers).forEach(function(key){ 16 | req.setRequestHeader(key, headers[key]) 17 | }) 18 | 19 | req.onload = function() { 20 | cb(req.status, req.responseText) 21 | } 22 | 23 | req.onerror = req.onabort = function() { 24 | disableSubmit(false) 25 | error(el, 'Sorry, failed to upload file.') 26 | } 27 | 28 | req.upload.onprogress = function(data) { 29 | progressBar(el, data, showProgress) 30 | } 31 | 32 | req.send(data) 33 | } 34 | 35 | var parseURL = function(text) { 36 | var xml = new DOMParser().parseFromString(text, 'text/xml'), 37 | tag = xml.getElementsByTagName('Location')[0], 38 | url = unescape(tag.childNodes[0].nodeValue) 39 | 40 | return url 41 | } 42 | 43 | var parseJson = function(json) { 44 | var data 45 | try {data = JSON.parse(json)} 46 | catch(e){ data = null } 47 | return data 48 | } 49 | 50 | var progressBar = function(el, data, showProgress) { 51 | if(data.lengthComputable === false || showProgress === false) return 52 | 53 | var pcnt = Math.round(data.loaded * 100 / data.total), 54 | bar = el.querySelector('.bar') 55 | 56 | bar.style.width = pcnt + '%' 57 | } 58 | 59 | var error = function(el, msg) { 60 | el.className = 's3direct form-active' 61 | el.querySelector('.file-input').value = '' 62 | alert(msg) 63 | } 64 | 65 | var update = function(el, xml) { 66 | var link = el.querySelector('.file-link'), 67 | url = el.querySelector('.file-url') 68 | 69 | url.value = parseURL(xml) 70 | link.setAttribute('href', url.value) 71 | link.innerHTML = url.value.split('/').pop() 72 | 73 | el.className = 's3direct link-active' 74 | el.querySelector('.bar').style.width = '0%' 75 | } 76 | 77 | var concurrentUploads = 0 78 | var disableSubmit = function(status) { 79 | var submitRow = document.querySelector('.submit-row') 80 | if( ! submitRow) return 81 | 82 | var buttons = submitRow.querySelectorAll('input[type=submit]') 83 | 84 | if (status === true) concurrentUploads++ 85 | else concurrentUploads-- 86 | 87 | ;[].forEach.call(buttons, function(el){ 88 | el.disabled = (concurrentUploads !== 0) 89 | }) 90 | } 91 | 92 | var upload = function(file, data, el) { 93 | var form = new FormData() 94 | 95 | disableSubmit(true) 96 | 97 | if (data === null) return error(el, 'Sorry, could not get upload URL.') 98 | 99 | el.className = 's3direct progress-active' 100 | var url = data['form_action'] 101 | delete data['form_action'] 102 | 103 | Object.keys(data).forEach(function(key){ 104 | form.append(key, data[key]) 105 | }) 106 | form.append('file', file) 107 | 108 | request('POST', url, form, {}, el, true, function(status, xml){ 109 | disableSubmit(false) 110 | if(status !== 201) return error(el, 'Sorry, failed to upload to S3.') 111 | update(el, xml) 112 | }) 113 | } 114 | 115 | var getUploadURL = function(e) { 116 | var el = e.target.parentElement, 117 | file = el.querySelector('.file-input').files[0], 118 | dest = el.querySelector('.file-dest').value, 119 | url = el.getAttribute('data-policy-url'), 120 | form = new FormData(), 121 | headers = {'X-CSRFToken': getCookie('csrftoken')} 122 | 123 | form.append('type', file.type) 124 | form.append('name', file.name) 125 | form.append('dest', dest) 126 | 127 | request('POST', url, form, headers, el, false, function(status, json){ 128 | var data = parseJson(json) 129 | 130 | switch(status) { 131 | case 200: 132 | upload(file, data, el) 133 | break 134 | case 400: 135 | case 403: 136 | error(el, data.error) 137 | break; 138 | default: 139 | error(el, 'Sorry, could not get upload URL.') 140 | } 141 | }) 142 | } 143 | 144 | var removeUpload = function(e) { 145 | e.preventDefault() 146 | 147 | var el = e.target.parentElement 148 | el.querySelector('.file-url').value = '' 149 | el.querySelector('.file-input').value = '' 150 | el.className = 's3direct form-active' 151 | } 152 | 153 | var addHandlers = function(el) { 154 | var url = el.querySelector('.file-url'), 155 | input = el.querySelector('.file-input'), 156 | remove = el.querySelector('.file-remove'), 157 | status = (url.value === '') ? 'form' : 'link' 158 | 159 | el.className = 's3direct ' + status + '-active' 160 | 161 | remove.addEventListener('click', removeUpload, false) 162 | input.addEventListener('change', getUploadURL, false) 163 | } 164 | 165 | document.addEventListener('DOMContentLoaded', function(e) { 166 | ;[].forEach.call(document.querySelectorAll('.s3direct'), addHandlers) 167 | }) 168 | 169 | document.addEventListener('DOMNodeInserted', function(e){ 170 | if(e.target.tagName) { 171 | var el = e.target.querySelector('.s3direct') 172 | if(el) addHandlers(el) 173 | } 174 | }) 175 | 176 | })() -------------------------------------------------------------------------------- /s3direct/static/s3direct/css/bootstrap-progress.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.3.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;} 11 | .clearfix:after{clear:both;} 12 | .hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} 13 | .input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} 14 | @-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 15 | .progress .bar{width:0%;height:100%;color:#ffffff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;} 16 | .progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);} 17 | .progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;} 18 | .progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;} 19 | .progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(to bottom, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);} 20 | .progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} 21 | .progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(to bottom, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);} 22 | .progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} 23 | .progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(to bottom, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);} 24 | .progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} 25 | .progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);} 26 | .progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} 27 | --------------------------------------------------------------------------------