├── 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 |
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 | ''
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 | '
'
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 | [](https://travis-ci.org/bradleyg/django-s3direct)
8 | [](https://crate.io/packages/django-s3direct)
9 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------