Skip to content

Commit

Permalink
Merge pull request #7 from hannseman/master
Browse files Browse the repository at this point in the history
Adds support for setting custom HTTP headers, on files uploaded to S3
  • Loading branch information
e-dard committed Jun 11, 2013
2 parents 9412014 + c07be84 commit a7b8152
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -1,2 +1,7 @@
*.pyc
.DS_Store
*.egg
*.egg-info
dist
/.idea
_build
1 change: 1 addition & 0 deletions CONTRIBUTORS
Expand Up @@ -3,3 +3,4 @@ Contributors

* Edward Robinson (e-dard)
* Rehan Dalal (rehandalal)
* Hannes Ljungberg (hannseman)
28 changes: 21 additions & 7 deletions docs/index.rst
Expand Up @@ -139,6 +139,22 @@ generated by Flask-S3 will look like the following:
that ``mybucketname`` is the name of your S3 bucket, and you have chosen
to have assets served over HTTPS.

Setting Custom HTTP Headers
~~~~~~~~~~~~~~~~~

To set custom HTTP headers on the files served from S3 specify what
headers you want to use with the `S3_HEADERS` option.

.. code-block:: python
S3_HEADERS = {
'Expires': 'Thu, 15 Apr 2010 20:00:00 GMT',
'Cache-Control': 'max-age=86400',
}
See `Yahoo!`_ more information on how to set good values for your headers.

.. _Yahoo!: http://developer.yahoo.com/performance/rules.html#expires

.. _settings:
.. _configuration:
Expand All @@ -147,7 +163,7 @@ Flask-S3 Options
----------------

Within your Flask application's settings you can provide the following
settings to control the behvaiour of Flask-S3. None of the settings are
settings to control the behaviour of Flask-S3. None of the settings are
required, but if not present, some will need to be provided when
uploading assets to S3.

Expand All @@ -163,12 +179,6 @@ uploading assets to S3.
**Default:** ``u's3.amazonaws.com'``
`S3_BUCKET_NAME` The desired name for your Amazon S3 bucket. Note:
the name will be visible in all your assets' URLs.
`S3_CACHE_CONTROL` This sets the value of the Cache-Control header that
is set in the metadata when `S3_USE_CACHE_CONTRL` is
set to `True`.
`S3_USE_CACHE_CONTROL` Specifies whether or not to set the metadata for the
Cache-Control headers.
**Default:** `False`
`S3_USE_HTTPS` Specifies whether or not to serve your assets
stored in S3 over HTTPS.
**Default:** `True`
Expand All @@ -186,6 +196,10 @@ uploading assets to S3.
**Note**: if `USE_S3` is set to `False` then
templates will always include asset locations
specified by `flask.url_for`.
`S3_HEADERS` Sets custom headers to be sent with each file to S3.
**Default:** `{}`
`S3_CACHE_CONTROL` **Deprecated**. Please use `S3_HEADERS` instead.
`S3_USE_CACHE_CONTROL` **Deprecated**. Please use `S3_HEADERS` instead.
=========================== ===================================================

.. _debug: http://flask.pocoo.org/docs/config/#configuration-basics
Expand Down
39 changes: 25 additions & 14 deletions flask_s3.py
Expand Up @@ -10,6 +10,7 @@

logger = logging.getLogger('flask_s3')


def url_for(endpoint, **values):
"""
Generates a URL to the given endpoint.
Expand All @@ -27,33 +28,35 @@ def url_for(endpoint, **values):
app = current_app
if 'S3_BUCKET_NAME' not in app.config:
raise ValueError("S3_BUCKET_NAME not found in app configuration.")

if app.debug and not app.config['USE_S3_DEBUG']:
return flask_url_for(endpoint, **values)

if endpoint == 'static' or endpoint.endswith('.static'):
scheme = 'http'
if app.config['S3_USE_HTTPS']:
scheme = 'https'
bucket_path = '%s.%s' % (app.config['S3_BUCKET_NAME'],
bucket_path = '%s.%s' % (app.config['S3_BUCKET_NAME'],
app.config['S3_BUCKET_DOMAIN'])
urls = app.url_map.bind(bucket_path, url_scheme=scheme)
return urls.build(endpoint, values=values, force_external=True)
return flask_url_for(endpoint, **values)


def _bp_static_url(blueprint):
""" builds the absolute url path for a blueprint's static folder """
u = u'%s%s' % (blueprint.url_prefix or '', blueprint.static_url_path or '')
return u


def _gather_files(app, hidden):
""" Gets all files in static folders and returns in dict."""
dirs = [(unicode(app.static_folder), app.static_url_path)]
if hasattr(app, 'blueprints'):
blueprints = app.blueprints.values()
bp_details = lambda x: (x.static_folder, _bp_static_url(x))
dirs.extend([bp_details(x) for x in blueprints if x.static_folder])

valid_files = defaultdict(list)
for static_folder, static_url_loc in dirs:
if not os.path.isdir(static_folder):
Expand All @@ -67,10 +70,12 @@ def _gather_files(app, hidden):
valid_files[(static_folder, static_url_loc)].extend(files)
return valid_files


def _path_to_relative_url(path):
""" Converts a folder and filename into a ralative url path """
return os.path.splitdrive(path)[1].replace('\\', '/')


def _static_folder_path(static_url, static_folder, static_asset):
"""
Returns a path to a file based on the static folder, and not on the
Expand All @@ -88,30 +93,33 @@ def _static_folder_path(static_url, static_folder, static_asset):
# Now bolt the static url path and the relative asset location together
return u'%s/%s' % (static_url.rstrip('/'), rel_asset.lstrip('/'))

def _write_files(app, static_url_loc, static_folder, files, bucket,

def _write_files(app, static_url_loc, static_folder, files, bucket,
ex_keys=None):
""" Writes all the files inside a static folder to S3. """
for file_path in files:
asset_loc = _path_to_relative_url(file_path)
key_name = _static_folder_path(static_url_loc, static_folder,
key_name = _static_folder_path(static_url_loc, static_folder,
asset_loc)
msg = "Uploading %s to %s as %s" % (file_path, bucket, key_name)
logger.debug(msg)
if ex_keys and key_name in ex_keys:
logger.debug("%s excluded from upload" % key_name)
else:
k = Key(bucket=bucket, name=key_name)
if (app.config['S3_USE_CACHE_CONTROL'] and
'S3_CACHE_CONTROL' in app.config):
k.set_metadata('Cache-Control', app.config['S3_CACHE_CONTROL'])
# Set custom headers
for header, value in app.config['S3_HEADERS'].iteritems():
k.set_metadata(header, value)
k.set_contents_from_filename(file_path)
k.make_public()


def _upload_files(app, files_, bucket):
for (static_folder, static_url), names in files_.iteritems():
_write_files(app, static_url, static_folder, names, bucket)

def create_all(app, user=None, password=None, bucket_name=None,

def create_all(app, user=None, password=None, bucket_name=None,
location='', include_hidden=False):
"""
Uploads of the static assets associated with a Flask application to
Expand Down Expand Up @@ -207,14 +215,17 @@ def init_app(self, app):
:param app: the :class:`flask.Flask` application object.
"""
defaults = [('S3_USE_HTTPS', True),
('USE_S3', True),
defaults = [('S3_USE_HTTPS', True),
('USE_S3', True),
('USE_S3_DEBUG', False),
('S3_BUCKET_DOMAIN', 's3.amazonaws.com'),
('S3_USE_CACHE_CONTROL', False)]
('S3_USE_CACHE_CONTROL', False),
('S3_HEADERS', {})]
for k, v in defaults:
app.config.setdefault(k, v)

if app.config['USE_S3']:
app.jinja_env.globals['url_for'] = url_for

if app.config['S3_USE_CACHE_CONTROL'] and 'S3_CACHE_CONTROL' in app.config:
cache_control_header = app.config['S3_CACHE_CONTROL']
app.config['S3_HEADERS']['Cache-Control'] = cache_control_header
30 changes: 18 additions & 12 deletions tests/test_flask_static.py
Expand Up @@ -7,6 +7,7 @@
import flask_s3
from flask_s3 import FlaskS3


class FlaskStaticTest(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
Expand All @@ -28,12 +29,11 @@ def test_config(self):
""" Tests configuration vars exist. """
FlaskS3(self.app)
defaults = ('S3_USE_HTTPS', 'USE_S3', 'USE_S3_DEBUG',
'S3_BUCKET_DOMAIN', 'S3_USE_CACHE_CONTROL')
'S3_BUCKET_DOMAIN', 'S3_USE_CACHE_CONTROL', 'S3_HEADERS')
for default in defaults:
self.assertIn(default, self.app.config)



class UrlTests(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
Expand Down Expand Up @@ -118,6 +118,10 @@ def setUp(self):
self.app.config['S3_BUCKET_NAME'] = 'foo'
self.app.config['S3_USE_CACHE_CONTROL'] = True
self.app.config['S3_CACHE_CONTROL'] = 'cache instruction'
self.app.config['S3_HEADERS'] = {
'Expires': 'Thu, 31 Dec 2037 23:59:59 GMT',
'Content-Encoding': 'gzip',
}

def test__bp_static_url(self):
""" Tests test__bp_static_url """
Expand All @@ -141,12 +145,12 @@ def test__gather_files(self, path_mock, os_mock):
url_prefix=None)
bp_c = Mock(static_folder=None)

self.app.blueprints = { 'a': bp_a, 'b': bp_b, 'c': bp_c}
dirs = { '/home': [('/home', None, ['.a'])],
'/home/bar': [('/home/bar', None, ['b'])],
'/home/zoo': [('/home/zoo', None, ['c']),
('/home/zoo/foo', None, ['d', 'e'])] }
os_mock.side_effect=dirs.get
self.app.blueprints = {'a': bp_a, 'b': bp_b, 'c': bp_c}
dirs = {'/home': [('/home', None, ['.a'])],
'/home/bar': [('/home/bar', None, ['b'])],
'/home/zoo': [('/home/zoo', None, ['c']),
('/home/zoo/foo', None, ['d', 'e'])]}
os_mock.side_effect = dirs.get
path_mock.return_value = True

expected = {('/home/bar', u'/a/bar'): ['/home/bar/b'],
Expand All @@ -169,7 +173,7 @@ def test__gather_files_no_blueprints_no_files(self, path_mock, os_mock):
"""
self.app.static_folder = '/foo'
dirs = {'/foo': [('/foo', None, [])]}
os_mock.side_effect=dirs.get
os_mock.side_effect = dirs.get
path_mock.return_value = True

actual = flask_s3._gather_files(self.app, False)
Expand All @@ -183,7 +187,7 @@ def test__gather_files_bad_folder(self, path_mock, os_mock):
"""
self.app.static_folder = '/bad'
dirs = {'/bad': []}
os_mock.side_effect=dirs.get
os_mock.side_effect = dirs.get
path_mock.return_value = False

actual = flask_s3._gather_files(self.app, False)
Expand All @@ -209,7 +213,9 @@ def test__write_files(self, key_mock):
exclude = ['/foo/static/foo.css', '/foo/static/foo/bar.css']
# we expect foo.css to be excluded and not uploaded
expected = [call(bucket=None, name=u'/foo/static/bar.css'),
call().set_metadata('Cache-Control', 'cache instruction'),
call().set_metadata('Cache-Control', 'cache instruction'),
call().set_metadata('Expires', 'Thu, 31 Dec 2037 23:59:59 GMT'),
call().set_metadata('Content-Encoding', 'gzip'),
call().set_contents_from_filename('/home/z/bar.css')]
flask_s3._write_files(self.app, static_url_loc, static_folder, assets,
None, exclude)
Expand All @@ -226,4 +232,4 @@ def test_static_folder_path(self):
self.assertEquals(e, flask_s3._static_folder_path(*i))

if __name__ == '__main__':
unittest.main()
unittest.main()

0 comments on commit a7b8152

Please sign in to comment.