Permalink
Browse files

Added support for send_file

  • Loading branch information...
1 parent 4156bd4 commit 2d87e9bc37ef1ce8f9068fac92783a4fe3d3d382 @mitsuhiko mitsuhiko committed May 10, 2010
Showing with 161 additions and 2 deletions.
  1. +1 −0 CHANGES
  2. +2 −0 docs/api.rst
  3. +74 −1 flask.py
  4. +84 −1 tests/flask_tests.py
View
@@ -15,6 +15,7 @@ Version 0.2
view function.
- server listens on 127.0.0.1 by default now to fix issues with chrome.
- added external URL support.
+- added support for :func:`~flask.send_file`
Version 0.1
-----------
View
@@ -213,6 +213,8 @@ Useful Functions and Classes
.. autofunction:: redirect
+.. autofunction:: send_file
+
.. autofunction:: escape
.. autoclass:: Markup
View
@@ -12,12 +12,13 @@
from __future__ import with_statement
import os
import sys
+import mimetypes
from datetime import datetime, timedelta
from jinja2 import Environment, PackageLoader, FileSystemLoader
from werkzeug import Request as RequestBase, Response as ResponseBase, \
LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \
- ImmutableDict, cached_property
+ ImmutableDict, cached_property, wrap_file, Headers
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException
from werkzeug.contrib.securecookie import SecureCookie
@@ -235,6 +236,71 @@ def get_current_user():
indent=None if request.is_xhr else 2), mimetype='application/json')
+def send_file(filename_or_fp, mimetype=None, as_attachment=False,
+ attachment_filename=None):
+ """Sends the contents of a file to the client. This will use the
+ most efficient method available and configured. By default it will
+ try to use the WSGI server's file_wrapper support. Alternatively
+ you can set the application's :attr:`~Flask.use_x_sendfile` attribute
+ to ``True`` to directly emit an `X-Sendfile` header. This however
+ requires support of the underlying webserver for `X-Sendfile`.
+
+ By default it will try to guess the mimetype for you, but you can
+ also explicitly provide one. For extra security you probably want
+ to sent certain files as attachment (HTML for instance).
+
+ .. versionadded:: 0.2
+
+ :param filename_or_fp: the filename of the file to send. This is
+ relative to the :attr:`~Flask.root_path` if a
+ relative path is specified.
+ Alternatively a file object might be provided
+ in which case `X-Sendfile` might not work and
+ fall back to the traditional method.
+ :param mimetype: the mimetype of the file if provided, otherwise
+ auto detection happens.
+ :param as_attachment: set to `True` if you want to send this file with
+ a ``Content-Disposition: attachment`` header.
+ :param attachment_filename: the filename for the attachment if it
+ differs from the file's filename.
+ """
+ if isinstance(filename_or_fp, basestring):
+ filename = filename_or_fp
+ file = None
+ else:
+ file = filename_or_fp
+ filename = getattr(file, 'name', None)
+ if filename is not None:
+ filename = os.path.join(current_app.root_path, filename)
+ if mimetype is None and (filename or attachment_filename):
+ mimetype = mimetypes.guess_type(filename or attachment_filename)[0]
+ if mimetype is None:
+ mimetype = 'application/octet-stream'
+
+ headers = Headers()
+ if as_attachment:
+ if attachment_filename is None:
+ if filename is None:
+ raise TypeError('filename unavailable, required for '
+ 'sending as attachment')
+ attachment_filename = os.path.basename(filename)
+ headers.add('Content-Disposition', 'attachment',
+ filename=attachment_filename)
+
+ if current_app.use_x_sendfile and filename:
+ if file is not None:
+ file.close()
+ headers['X-Sendfile'] = filename
+ data = None
+ else:
+ if file is None:
+ file = open(filename, 'rb')
+ data = wrap_file(request.environ, file)
+
+ return Response(data, mimetype=mimetype, headers=headers,
+ direct_passthrough=True)
+
+
def render_template(template_name, **context):
"""Renders a template from the template folder with the given
context.
@@ -344,6 +410,13 @@ class Flask(object):
#: permanent session survive for roughly one month.
permanent_session_lifetime = timedelta(days=31)
+ #: Enable this if you want to use the X-Sendfile feature. Keep in
+ #: mind that the server has to support this. This only affects files
+ #: sent with the :func:`send_file` method.
+ #:
+ #: .. versionadded:: 0.2
+ use_x_sendfile = False
+
#: options that are passed directly to the Jinja2 environment
jinja_options = ImmutableDict(
autoescape=True,
View
@@ -17,7 +17,8 @@
import unittest
import tempfile
from datetime import datetime
-from werkzeug import parse_date
+from werkzeug import parse_date, parse_options_header
+from cStringIO import StringIO
example_path = os.path.join(os.path.dirname(__file__), '..', 'examples')
@@ -382,13 +383,95 @@ def index():
assert rv.data == 'dcba'
+class SendfileTestCase(unittest.TestCase):
+
+ def test_send_file_regular(self):
+ app = flask.Flask(__name__)
+ with app.test_request_context():
+ rv = flask.send_file('static/index.html')
+ assert rv.direct_passthrough
+ assert rv.mimetype == 'text/html'
+ with app.open_resource('static/index.html') as f:
+ assert rv.data == f.read()
+
+ def test_send_file_xsendfile(self):
+ app = flask.Flask(__name__)
+ app.use_x_sendfile = True
+ with app.test_request_context():
+ rv = flask.send_file('static/index.html')
+ assert rv.direct_passthrough
+ assert 'x-sendfile' in rv.headers
+ assert rv.headers['x-sendfile'] == \
+ os.path.join(app.root_path, 'static/index.html')
+ assert rv.mimetype == 'text/html'
+
+ def test_send_file_object(self):
+ app = flask.Flask(__name__)
+ with app.test_request_context():
+ f = open(os.path.join(app.root_path, 'static/index.html'))
+ rv = flask.send_file(f)
+ with app.open_resource('static/index.html') as f:
+ assert rv.data == f.read()
+ assert rv.mimetype == 'text/html'
+
+ app.use_x_sendfile = True
+ with app.test_request_context():
+ f = open(os.path.join(app.root_path, 'static/index.html'))
+ rv = flask.send_file(f)
+ assert rv.mimetype == 'text/html'
+ assert 'x-sendfile' in rv.headers
+ assert rv.headers['x-sendfile'] == \
+ os.path.join(app.root_path, 'static/index.html')
+
+ app.use_x_sendfile = False
+ with app.test_request_context():
+ f = StringIO('Test')
+ rv = flask.send_file(f)
+ assert rv.data == 'Test'
+ assert rv.mimetype == 'application/octet-stream'
+ f = StringIO('Test')
+ rv = flask.send_file(f, mimetype='text/plain')
+ assert rv.data == 'Test'
+ assert rv.mimetype == 'text/plain'
+
+ app.use_x_sendfile = True
+ with app.test_request_context():
+ f = StringIO('Test')
+ rv = flask.send_file(f)
+ assert 'x-sendfile' not in rv.headers
+
+ def test_attachment(self):
+ app = flask.Flask(__name__)
+ with app.test_request_context():
+ f = open(os.path.join(app.root_path, 'static/index.html'))
+ rv = flask.send_file(f, as_attachment=True)
+ value, options = parse_options_header(rv.headers['Content-Disposition'])
+ assert value == 'attachment'
+
+ with app.test_request_context():
+ assert options['filename'] == 'index.html'
+ rv = flask.send_file('static/index.html', as_attachment=True)
+ value, options = parse_options_header(rv.headers['Content-Disposition'])
+ assert value == 'attachment'
+ assert options['filename'] == 'index.html'
+
+ with app.test_request_context():
+ rv = flask.send_file(StringIO('Test'), as_attachment=True,
+ attachment_filename='index.txt')
+ assert rv.mimetype == 'text/plain'
+ value, options = parse_options_header(rv.headers['Content-Disposition'])
+ assert value == 'attachment'
+ assert options['filename'] == 'index.txt'
+
+
def suite():
from minitwit_tests import MiniTwitTestCase
from flaskr_tests import FlaskrTestCase
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(ContextTestCase))
suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase))
suite.addTest(unittest.makeSuite(TemplatingTestCase))
+ suite.addTest(unittest.makeSuite(SendfileTestCase))
if flask.json_available:
suite.addTest(unittest.makeSuite(JSONTestCase))
suite.addTest(unittest.makeSuite(MiniTwitTestCase))

0 comments on commit 2d87e9b

Please sign in to comment.