From 58fa088abc0f0e511d934e822f917b796d418bb3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 03:46:42 +0200 Subject: [PATCH 1/4] Added missing file (fileupload docs) --- docs/patterns/fileuploads.rst | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/patterns/fileuploads.rst diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst new file mode 100644 index 00000000..fd94605d --- /dev/null +++ b/docs/patterns/fileuploads.rst @@ -0,0 +1,158 @@ +Uploading Files +=============== + +Ah yes, the good old problem of file uploads. The basic idea of file +uploads is actually quite simple. It basically works like this: + +1. A ``
`` tag is marked with ``enctype=multipart/form-data`` + and an ```` is placed in that form. +2. The application accesses the file from the :attr:`~flask.request.files` + dictionary on the request object. +3. use the :meth:`~werkzeug.FileStorage.save` method of the file to save + the file permanently somewhere on the filesystem. + +A Gentle Introduction +--------------------- + +Let's start with a very basic application that uploads a file to a +specific upload folder and displays a file to the user. Let's look at the +bootstrapping code for our application:: + + import os + from flask import Flask, request, redirect, url_for + from werkzeug import secure_filename + + UPLOAD_FOLDER = '/path/to/the/uploads' + ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) + + app = Flask(__name__) + app.add_url_rule('/uploads/', 'uploaded_file', + build_only=True) + +So first we need a couple of imports. Most should be straightforward, the +:func:`werkzeug.secure_filename` is explained a little bit later. The +`UPLOAD_FOLDER` is where we will store the uploaded files and the +`ALLOWED_EXTENSIONS` is the set of allowed file extensions. Then we add a +URL rule by hand to the application. Now usually we're not doing that, so +why here? The reasons is that we want the webserver (or our development +server) to serve these files for us and so we only need a rule to generate +the URL to these files. + +Why do we limit the extensions that are allowed? You probably don't want +your users to be able to upload everything there if the server is directly +sending out the data to the client. That way you can make sure that users +are not able to upload HTML files that would cause XSS problems. Also +make sure to disallow `.php` files if the server executes them, but who +has PHP installed on his server, right? :) + +Next the functions that check if an extension is valid and that uploads +the file and redirects the user to the URL for the uploaded file:: + + def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + + @app.route('/') + def upload_file(): + if request.method == 'POST': + file = request.files['file'] + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file.save(os.path.join(UPLOAD_FOLDER, filename)) + return redirect(url_for('uploaded_file', + filename=filename)) + return ''' + + Upload new File +

Upload new File

+ +

+ +

+ ''' + +So what does that :func:`~werkzeug.secure_filename` function actually do? +Now the problem is that there is that principle called "never trust user +input". This is also true for the filename of an uploaded file. All +submitted form data can be forged, and filenames can be dangerous. For +the moment just remember: always use that function to secure a filename +before storing it directly on the filesystem. + +.. admonition:: Information for the Pros + + So you're interested in what that :func:`~werkzeug.secure_filename` + function does and what the problem is if you're not using it? So just + imagine someone would send the following information as `filename` to + your application:: + + filename = "../../../../home/username/.bashrc" + + Assuming the number of ``../`` is correct and you would join this with + the `UPLOAD_FOLDER` the user might have the ability to modify a file on + the server's filesystem he should not modify. This does require some + knowledge about how the application looks like, but trust me, hackers + are patient :) + + Now let's look how that function works: + + >>> secure_filename('../../../../home/username/.bashrc') + 'home_username_.bashrc' + +Now if we run that application, you will notice that uploading works, but +you won't actually see that uploaded file. Well, you would have to +configure the server to serve that file for you. This is not handy for +development situations or when you are just too lazy to properly set up +the server. Would be nice to have the files still be available in that +situation, and that is really easy to do, just hook in a middleware:: + + from werkzeug import SharedDataMiddleware + app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { + '/uploads': UPLOAD_FOLDER + }) + +If you now run the application everything should work as expected. + + +Improving Uploads +----------------- + +So how exactly does Flask handle uploads? Well it will store them in the +webserver's memory if the files are reasonable small otherwise in a +temporary location (as returned by :func:`tempfile.gettempdir`). But how +do you specify the maximum file size after which an upload is aborted? By +default Flask will happily accept file uploads to an unlimited amount of +memory, but you can limit that by subclassing the request and overriding +the Werkzeug provided :attr:`~werkzeug.BaseRequest.max_form_memory_size` +attribute:: + + from flask import Flask, Request + + class LimitedRequest(Request): + max_form_memory_size = 16 * 1024 * 1024 + + app = Flask(__name__) + app.request_class = LimitedRequest + +The code above will limited the maximum allowed payload to 16 megabytes. +If a larger file is transmitted, Flask will raise an +:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. + + +Upload Progress Bars +-------------------- + +A while ago many developers had the idea to read the incoming file in +small chunks and store the upload progress in the database to be able to +poll the progress with JavaScript from the client. Long story short: the +client asks the server every 5 seconds how much he has transmitted +already. Do you realize the irony? The client is asking for something he +should already know. + +Now there are better solutions to that work faster and more reliable. The +web changed a lot lately and you can use HTML5, Java, Silverlight or Flash +to get a nicer uploading experience on the client side. Look at the +following libraries for some nice examples how to do that: + +- `Plupload `_ - HTML5, Java, Flash +- `SWFUpload `_ - Flash +- `JumpLoader `_ - Java From a9bb965b6dcd931cefdc7dd05fa4c672b5dff69c Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 03:44:11 +0800 Subject: [PATCH 2/4] add a decorator to add custom template filter --- flask.py | 22 ++++++++++++++++++++++ tests/flask_tests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/flask.py b/flask.py index 10feaf22..6d557b26 100644 --- a/flask.py +++ b/flask.py @@ -12,6 +12,7 @@ from __future__ import with_statement import os import sys +import types from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ @@ -639,6 +640,27 @@ class Flask(object): return f return decorator + def template_filter(self, arg=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + if type(arg) is types.FunctionType: + self.jinja_env.filters[arg.__name__] = arg + return arg + + def decorator(f): + self.jinja_env.filters[arg or f.__name__] = f + return f + return decorator + def before_request(self, f): """Registers a function to run before each request.""" self.before_request_funcs.append(f) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 917f4168..91edb9c2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -311,6 +311,32 @@ class TemplatingTestCase(unittest.TestCase): macro = flask.get_template_attribute('_macro.html', 'hello') assert macro('World') == 'Hello World!' + def test_template_filter_not_called(self): + app = flask.Flask(__name__) + @app.template_filter + def my_reverse(s): + return s[::-1] + assert 'my_reverse' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['my_reverse'] == my_reverse + assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + + def test_template_filter_called(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + assert 'my_reverse' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['my_reverse'] == my_reverse + assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + assert 'strrev' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['strrev'] == my_reverse + assert app.jinja_env.filters['strrev']('abcd') == 'dcba' def suite(): from minitwit_tests import MiniTwitTestCase From 5c9ef2c44dce5afc99a0b3b539ebaafe9b8ce3c5 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 17:39:19 +0800 Subject: [PATCH 3/4] the template_filter now expects the parentheses --- flask.py | 10 +++------- tests/flask_tests.py | 11 +---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/flask.py b/flask.py index 6d557b26..216f9af8 100644 --- a/flask.py +++ b/flask.py @@ -640,24 +640,20 @@ class Flask(object): return f return decorator - def template_filter(self, arg=None): + def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: - @app.template_filter + @app.template_filter() def reverse(s): return s[::-1] :param name: the optional name of the filter, otherwise the function name will be used. """ - if type(arg) is types.FunctionType: - self.jinja_env.filters[arg.__name__] = arg - return arg - def decorator(f): - self.jinja_env.filters[arg or f.__name__] = f + self.jinja_env.filters[name or f.__name__] = f return f return decorator diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 91edb9c2..6f5a4d38 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -311,16 +311,7 @@ class TemplatingTestCase(unittest.TestCase): macro = flask.get_template_attribute('_macro.html', 'hello') assert macro('World') == 'Hello World!' - def test_template_filter_not_called(self): - app = flask.Flask(__name__) - @app.template_filter - def my_reverse(s): - return s[::-1] - assert 'my_reverse' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['my_reverse'] == my_reverse - assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' - - def test_template_filter_called(self): + def test_template_filter(self): app = flask.Flask(__name__) @app.template_filter() def my_reverse(s): From 4395e9493cc33d49b53889f9f726458f05f1a475 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 20:44:24 +0800 Subject: [PATCH 4/4] add tests for template_filter using a real template --- tests/flask_tests.py | 23 +++++++++++++++++++++++ tests/templates/template_filter.html | 1 + 2 files changed, 24 insertions(+) create mode 100644 tests/templates/template_filter.html diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6f5a4d38..b976015a 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -329,6 +329,29 @@ class TemplatingTestCase(unittest.TestCase): assert app.jinja_env.filters['strrev'] == my_reverse assert app.jinja_env.filters['strrev']('abcd') == 'dcba' + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase diff --git a/tests/templates/template_filter.html b/tests/templates/template_filter.html new file mode 100644 index 00000000..af46cd94 --- /dev/null +++ b/tests/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} \ No newline at end of file