Загрузка файлов в веб-приложении: разбор на живом проекте
Файл нельзя считать безопасным только потому, что его принял HTTP-сервер. Разбираем загрузку в приложении на Django 5.2: где проверять размер и тип, когда нужен Nginx, а когда — объектное хранилище.

На проекте с загрузкой документов проблема появилась не в самой форме: пользователи отправляли файлы на 200 МБ, приложение держало их в памяти дольше ожидаемого, а ссылки на старые версии продолжали работать. Разберём схему для Django 5.2 и PostgreSQL: как провести файл через приложение, где поставить лимиты, что оставить локальному диску, а что вынести в объектное хранилище.
Исходные условия проекта
Представим сервис, в котором пользователь прикладывает PDF, изображения и офисные документы к заявке. Веб-приложение работает на Django 5.2 за Nginx, файлы доступны авторизованным пользователям, а резервные копии базы и медиа должны восстанавливаться независимо друг от друга.
Наивная схема выглядит так: форма принимает файл, Django сохраняет его в MEDIA_ROOT, а приложение отдаёт ссылку из FileField. Для небольшой внутренней системы этого может хватить. Но у решения есть цена: медиа занимают место на сервере, попадают в резервную копию вместе с приложением и становятся уязвимым местом при горизонтальном масштабировании.
Загрузка файла — это не одно действие, а цепочка из лимита, проверки, хранения, выдачи и удаления. Если контролировать только форму, остальные звенья всё равно могут сломаться.
Три схемы и их цена
В проекте стоит выбирать не «самое современное» хранилище, а границу ответственности. Приложение должно решить, можно ли принимать файл и кому его показывать. Хранилище — сохранить байты и отдать их без потери целостности.
| Схема | Когда подходит | Что выигрываете | За что платите |
|---|---|---|---|
| Локальный диск приложения | Один сервер, небольшие объёмы, внутренний сервис | Простая настройка, нет отдельного хранилища | Нужно отдельно делать резервные копии и синхронизацию |
| Nginx принимает файл, приложение получает путь | Большие файлы и ограниченный бюджет CPU приложения | Меньше нагрузки на Python-процесс | Сложнее связать загрузку с правами и откатом записи |
| Объектное хранилище | Несколько экземпляров приложения, много медиа, CDN | Масштабирование, отдельные политики хранения, независимые бэкапы | Нужны настройка доступа, жизненный цикл объектов и контроль стоимости |
Для разбираемого проекта разумный первый этап — локальный диск за пределами каталога с кодом. Если появляются второй экземпляр приложения, десятки гигабайт медиа или требование раздавать файлы через CDN, переходят на объектное хранилище. Переписывать бизнес-логику при этом не придётся, если приложение не строит путь к файлу вручную.
Граница приложения: модель и форма
В Django 5.2 файл обычно описывают через FileField. Поле хранит не сами байты в PostgreSQL, а имя объекта в хранилище. Поэтому удаление строки из базы не означает автоматическое удаление файла с диска или из объектного хранилища.
from django.db import models
class Attachment(models.Model):
file = models.FileField(upload_to="attachments/%Y/%m/")
uploaded_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.file.name
Путь из upload_to помогает не складывать тысячи объектов в один каталог. Он не является защитой: имя файла и его расширение нельзя считать доказательством типа содержимого.
Проверку размера и допустимого расширения нужно повторять на сервере, даже если браузер уже ограничивает выбор файла. Пример ниже рассчитан на обычную Django-форму и не использует доверие к имени файла.
from pathlib import Path
from django import forms
from .models import Attachment
class AttachmentForm(forms.ModelForm):
class Meta:
model = Attachment
fields = ("file",)
def clean_file(self):
uploaded_file = self.cleaned_data["file"]
max_size = 10 * 1024 * 1024
allowed_extensions = {"pdf", "jpg", "jpeg", "png"}
if uploaded_file.size > max_size:
raise forms.ValidationError("Файл больше 10 МБ.")
extension = Path(uploaded_file.name).suffix.lower().lstrip(".")
if extension not in allowed_extensions:
raise forms.ValidationError("Разрешены только PDF, JPG и PNG.")
return uploaded_file
Это только первый фильтр. Для документов, которые скачивают другие люди, дополнительно проверяйте фактический формат содержимого специализированным антивирусным или анализирующим сервисом. Значение content_type, присланное клиентом, удобно для предварительной проверки, но не должно быть единственным решением о безопасности.
Как файл проходит через систему
- Клиент отправляет multipart-запрос. Сервер проверяет размер запроса на уровне Nginx и приложения. Проверка на одном уровне не заменяет вторую: разные маршруты могут иметь разные ограничения. Проверка успешна, если слишком большой запрос получает предсказуемый ответ 413 или сообщение формы, а не тайм-аут.
- Django валидирует поле. Проверяются размер, расширение, права текущего пользователя и связь с заявкой. Проверка успешна, если неподходящий файл не создаёт запись и не оставляет бесхозный объект в хранилище.
- Файл получает имя хранилища. Нельзя использовать исходное имя как единственный идентификатор: в нём бывают пробелы, неоднозначные расширения и попытки изменить путь. Проверка успешна, если имя объекта формируется приложением, а пользовательское имя сохраняется отдельно как метаданные, если оно нужно интерфейсу.
- Сохраняется связь с бизнес-объектом. Запись о вложении должна ссылаться на заявку и пользователя. Проверка успешна, если запрос к API не позволяет прочитать или удалить чужой файл, даже если известен его URL.
- Файл выдаётся через контролируемый маршрут. Для приватных документов не публикуйте постоянный прямой URL. Проверка успешна, если без авторизации или нужной роли сервер возвращает отказ, а не содержимое файла.
Где очевидные решения ломаются
Ограничили размер только в браузере
Атрибут accept подсказывает браузеру, какие файлы показывать в диалоге выбора, но не является серверной проверкой. Клиент можно заменить curl-запросом или собственным скриптом. Лимит должен действовать в Nginx, веб-приложении и, при необходимости, в политике объектного хранилища.
Проверили только расширение
Файл report.pdf может содержать другой формат или вредоносное содержимое. Расширение полезно как правило интерфейса, но не как полноценная проверка. Для публичной выдачи добавьте проверку сигнатуры файла и антивирусный этап, а результат проверки храните в базе.
Отдали каталог media напрямую
Публичная раздача подходит для изображений, которые действительно должны быть доступны всем. Для договоров, паспортов и внутренних отчётов она превращает угадываемую или утёкшую ссылку в доступ к данным. Приватные файлы выдавайте через проверяемый маршрут или короткоживущую подписанную ссылку, если это поддерживает выбранное хранилище.
Удалили запись, но забыли объект
FileField не даёт универсальной гарантии удаления файла при удалении модели. Поэтому в проекте нужна явная политика: удалять объект в транзакционно согласованном процессе, отправлять задачу в очередь или периодически искать неиспользуемые объекты. Автоматическое удаление в сигнале удобно, но его нужно проверять при замене файла и откате бизнес-операции.
Что измерить до переноса в объектное хранилище
Без замеров нельзя честно назвать схему быстрой или дешёвой. На тестовом окружении зафиксируйте размер файлов, число параллельных загрузок и характеристики сервера. Отдельно измеряйте время приёма запроса, время сохранения объекта, пиковое потребление памяти процесса и долю неуспешных загрузок.
- размер тела запроса и фактический лимит на каждом слое;
- время от начала загрузки до появления записи о вложении;
- количество незавершённых и бесхозных объектов;
- место на диске и скорость его заполнения;
- коды 413, 499, 502 и ошибки тайм-аута в логах Nginx и приложения.
Сравнивайте результаты на одинаковых файлах и при одинаковом числе параллельных клиентов. Если тестов ещё нет, это не повод приписывать системе конкретную пропускную способность: сначала задайте сценарий и снимите метрики.
Когда выбирать каждую схему
Оставляйте локальный диск, если сервис работает на одном сервере, файлы не критичны для публичной доступности, а резервное копирование медиа уже проверено восстановлением. Выносите загрузку к Nginx, когда Python-процесс тратит время на передачу больших тел запросов, но вам нужна простая инфраструктура. Переходите к объектному хранилищу, когда приложение запускается в нескольких экземплярах или медиа должны жить независимо от жизненного цикла серверов.
Прямая загрузка из браузера в объектное хранилище снижает нагрузку на приложение, но усложняет выдачу временных разрешений, обработку прерванных загрузок и финальную проверку объекта. Это следующий этап архитектуры, а не автоматическая замена обычной Django-формы.
Что делать дальше
Сначала зафиксируйте допустимые форматы, максимальный размер и модель доступа для каждого типа вложений. Затем добавьте тесты на превышение лимита, подмену расширения, загрузку без прав, замену файла и удаление связанной записи. После этого настройте резервное копирование медиа и раз в квартал проверяйте восстановление на отдельном окружении.
Если проект растёт, следующим измерением станет доля загрузок, которые проходят через приложение целиком, и объём бесхозных объектов. Эти два показателя подскажут, пора ли выносить хранение в объектное хранилище и вводить отдельный процесс очистки.
Сноска. Бесхозный объект — файл, который уже лежит в хранилище, но больше не связан ни с одной актуальной записью приложения. Такие объекты незаметно увеличивают расходы и не всегда попадают под очистку базы данных.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Django 5.2 documentation: File uploads, Django 5.2 documentation: FileField
Частые вопросы
Достаточно ли проверить расширение файла?
Можно ли хранить файлы прямо в PostgreSQL?
Что произойдёт, если пользователь загрузит файл больше лимита?
Нужно ли делать отдельный маршрут для скачивания приватных файлов?
Как удалить файл, когда удаляется запись Django?
Что изменится, если у меня другая версия Django?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.