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:
- A tag is marked with enctype=multipart/form-data and an is placed in that form.
- The application accesses the file from the files dictionary on the request object.
- use the 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, flash, request, redirect, url_for from werkzeug.utils import secure_filename UPLOAD_FOLDER = '/path/to/the/uploads' ALLOWED_EXTENSIONS = 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'> app = Flask(__name__) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
So first we need a couple of imports. Most should be straightforward, the 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.
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 (see Cross-Site Scripting (XSS) ). Also make sure to disallow .php files if the server executes them, but who has PHP installed on their 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].lower() in ALLOWED_EXTENSIONS @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': # check if the post request has the file part if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] # if user does not select file, browser also # submit an empty part without filename if file.filename == '': flash('No selected file') return redirect(request.url) if file and allowed_file(file.filename): filename = secure_filename(file.filename) file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('uploaded_file', filename=filename)) return '''Upload new File
'''
So what does that 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.
So you’re interested in what that 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 or she 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 one last thing is missing: the serving of the uploaded files. In the upload_file() we redirect the user to url_for(‘uploaded_file’, filename=filename) , that is, /uploads/filename . So we write the uploaded_file() function to return the file of that name. As of Flask 0.5 we can use a function that does that for us:
from flask import send_from_directory @app.route('/uploads/') def uploaded_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
Alternatively you can register uploaded_file as build_only rule and use the SharedDataMiddleware . This also works with older versions of Flask:
from werkzeug.middleware.shared_data import SharedDataMiddleware app.add_url_rule('/uploads/', 'uploaded_file', build_only=True) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, '/uploads': app.config['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 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 setting the MAX_CONTENT_LENGTH config key:
from flask import Flask, Request app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
The code above will limit the maximum allowed payload to 16 megabytes. If a larger file is transmitted, Flask will raise a RequestEntityTooLarge exception.
When using the local development server, you may get a connection reset error instead of a 413 response. You will get the correct status response when running the app with a production WSGI server.
This feature was added in Flask 0.6 but can be achieved in older versions as well by subclassing the request object. For more information on that consult the Werkzeug documentation on file handling.
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 it has transmitted already. Do you realize the irony? The client is asking for something it should already know.
An Easier Solution¶
Now there are better solutions that work faster and are more reliable. There are JavaScript libraries like jQuery that have form plugins to ease the construction of progress bar.
Because the common pattern for file uploads exists almost unchanged in all applications dealing with uploads, there is also a Flask extension called Flask-Uploads that implements a full fledged upload mechanism with white and blacklisting of extensions and more.
Contents
Загрузка файлов¶
Ну да, старая добрая проблема загрузки файлов. Основная мысль загрузки файлов на самом деле очень проста. В общих чертах это работает так:
- Тег помечается атрибутом enctype=multipart/form-data , а в форму помещается тег .
- Приложение получает доступ к файлу через словарь files в объекте запроса.
- Воспользуйтесь методом save() для того, чтобы сохранить временный файл в файловой системе для последующего использования.
Введение¶
Начнём с простейшего приложения, которое загружает файл в определённый каталог и отображает его пользователю. Вот начало нашего приложения:
import os from flask import Flask, request, redirect, url_for from werkzeug.utils import secure_filename UPLOAD_FOLDER = '/path/to/the/uploads' ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) app = Flask(__name__) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
Сначала нужно выполнить серию импортов. Большая часть понятна, werkzeug.secure_filename() рассматривается чуть позже. UPLOAD_FOLDER — это путь, куда будут загружаться файлы, а ALLOWED_EXTENSIONS — это набор допустимых расширений. Теперь вручную добавим в приложение правило для URL. Обычно мы так не делаем, но почему мы делаем это сейчас? Причина в том, что мы хотим заставить веб-сервер (или наш сервер приложения) обслуживать эти файлы и поэтому нам нужно генерировать правила для связывания URL с этими файлами.
Почему мы ограничили список допустимых расширений? Наверное вам совсем не хочется, чтобы пользователи могли загружать что угодно, если сервер напрямую отправляет данные клиенту. В таком случае вам нужно быть уверенными в том, что пользователи не загрузят файлы HTML, которые могут вызвать проблему XSS (см. Cross-Site Scripting ( xss ) — межсайтовый скриптинг). Также убедитесь в том, что запрещены файлы с расширением .php , если сервер их выполняет. Правда, кому нужен PHP на сервере? 🙂
Следующая функция проверяет, что расширение файла допустимо, загружает файл и перенаправляет пользователя на URL с загруженным файлом:
def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS @app.route('/', methods=['GET', 'POST']) 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(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('uploaded_file', filename=filename)) return '''Upload new File
'''
Что делает функция secure_filename() ? Мы исходим из принципа «никогда не доверяй тому, что ввёл пользователь». Это справедливо и для имени загружаемого файла. Все отправленные из формы данные могут быть поддельными и имя файла может представлять опасность. Сейчас главное запомнить: всегда используйте эту функцию для получения безопасного имени файла, если собираетесь поместить файл прямо в файловую систему.
Может быть вам интересно, что делает функция secure_filename() и почему нельзя обойтись без её использования? Просто представьте, что кто-то хочет отправить следующую информацию в ваше приложение в качестве имени файла:
filename = "../../../../home/username/.bashrc"
Если считать, что ../ — это нормально, то при соединении этого имени с UPLOAD_FOLDER , пользователь может получить возможность изменять на файловой системе сервера те файлы, который он не должен изменять. Нужно немного разбираться в устройстве вашего приложения, но поверьте мне, хакеры настойчивы 🙂
Посмотрим, как отработает функция:
>>> secure_filename('../../../../home/username/.bashrc') 'home_username_.bashrc'
Осталась последняя вещь: обслуживание загруженных файлов. Начиная с Flask 0.5 для этого можно использовать соответствующую функцию:
from flask import send_from_directory @app.route('/uploads/') def uploaded_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
Другая возможность — зарегистрировать uploaded_file с помощью правила build_only и воспользоваться SharedDataMiddleware . Такой вариант будет работать и в более старых версиях Flask:
from werkzeug import SharedDataMiddleware app.add_url_rule('/uploads/', 'uploaded_file', build_only=True) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, '/uploads': app.config['UPLOAD_FOLDER'] >)
Теперь, если запустить приложение, всё должно работать как положено.
Улучшение загрузки¶
Как на самом деле Flask обрабатывает загрузку? Если файл достаточно мал, он сохраняется в памяти веб-сервера. В противном случае он помещается во временное место (туда, куда укажет tempfile.gettempdir() ). Но как указать максимальный размер файла, после которого загрузка файла должна быть прервана? По умолчанию Flask не ограничивает размер файла, но вы можете задать лимит настройкой ключа конфигурации MAX_CONTENT_LENGTH :
from flask import Flask, Request app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
Код выше ограничит максимальный размер файла 16 мегабайтами. Если передаваемый файл окажется больше, Flask сгенерирует исключение RequestEntityTooLarge .
Эта функциональность была добавлена во Flask 0.6, но может быть реализована и в более ранних версиях при помощи наследовании от класса request. За более подробной информацией обратитесь к документации Werkzeug об обработке файлов.
Индикаторы процесса загрузки¶
Многие разработчики придумывают считывать файл мелкими частями, сохранять процент загрузки в базу данных и давать возможность JavaScript считывать эти данные из клиента. Короче говоря, клиент спрашивает у сервера каждые 5 секунд, сколько уже было передано. Почувствовали иронию ситуации? Клиент спрашивает у сервера о том, что уже и так знает.
Сейчас существуют способы получше, которые работают быстрее и более надёжны. В последнее время в вебе многое изменилось и теперь можно использовать HTML5, Java, Silverlight или Flash, чтобы сделать загрузку удобнее со стороны клиента. Посмотрите на следующие библиотеки, предназначенные именно для этого:
Простейшее решение¶
Поскольку общая процедура загрузки файлов остаётся неизменной для всех приложений, занимающихся загрузкой файлов, для Flask есть расширение под названием Flask-Uploads, которое реализует полностью самостоятельный механизм загрузки с белым и чёрным списком расширений и т.п.