Загрузка нескольких файлов в Django: что знать до начала
Как настроить обработку multiple-загрузки файлов в Django 5.x: где ломается стандартная форма, что положить в MEDIA_ROOT и почему request.FILES.getlist важнее самого поля.

К концу этой инструкции у вас будет форма, которая принимает несколько файлов за один запрос, сохраняет их в отдельную папку и показывает список загруженного. На чистом проекте это занимает 20–30 минут. Главная ловушка ждёт в начале: стандартное поле forms.FileField с атрибутом multiple в шаблоне само по себе принимает только один файл — второй и третий Django молча отбросит, если не забрать их через getlist. Разберём по шагам, чтобы вы не потеряли час на отладке «почему сохранился только последний файл».
Что нужно иметь до начала
Инструкция написана для Django 5.x (проверял на 5.0 и 5.1). На 4.2 всё работает так же — расхождений в API загрузки файлов между этими версиями нет. Перед стартом убедитесь, что у вас есть:
- установленный Django и запущенный проект с приложением — команда
python manage.py runserverоткрывает страницу без ошибок; - настроенные
MEDIA_ROOTиMEDIA_URLвsettings.py— без них файлы физически некуда сохранять; - пакет
Pillow, если среди файлов будут изображения и вы планируетеImageField(для обычной загрузки файлов он не нужен); - права на запись в папку, куда указывает
MEDIA_ROOT— на своей машине это не проблема, на сервере проверьте отдельно.
Ещё одна вещь, которую узнают на пятом шаге и потом переделывают: HTML-форма для нескольких файлов должна иметь enctype="multipart/form-data". Без этого атрибута браузер отправит только имена файлов, а не их содержимое, и request.FILES будет пустым.
Шаг 1. Настройте MEDIA в settings.py
Django должен знать, куда складывать загруженное. Откройте settings.py и добавьте две строки (если их ещё нет):
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Папка на диске, куда физически пишутся файлы
MEDIA_ROOT = BASE_DIR / "media"
# URL-префикс, по которому файлы отдаются в режиме разработки
MEDIA_URL = "/media/"
Проверка: папка media в корне проекта появится автоматически при первой загрузке — создавать её руками не обязательно. Если хотите, создайте заранее.
Шаг 2. Разрешите отдачу файлов при разработке
Во время разработки Django сам не раздаёт медиа-файлы — это надо включить в корневом urls.py. Для боевого сервера так делать нельзя, там файлы отдаёт Nginx или объектное хранилище.
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from myapp import views
urlpatterns = [
path("admin/", admin.site.urls),
path("upload/", views.upload_files, name="upload_files"),
]
# Раздача медиа только в режиме отладки
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Проверка: сервер запускается без ошибок импорта. Если ругается на views.upload_files — представление вы напишете на шаге 4, пока можно закомментировать строку с path("upload/"...).
Шаг 3. Опишите форму с полем для нескольких файлов
В Django 5.x стандартный ClearableFileInput запрещает атрибут multiple — попытка передать его вызовет ошибку валидации. Поэтому нужен собственный виджет и поле, которые умеют работать со списком. Создайте forms.py:
from django import forms
class MultipleFileInput(forms.ClearableFileInput):
# Разрешаем виджету принимать несколько файлов
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_clean = super().clean
# data приходит списком, когда файлов несколько
if isinstance(data, (list, tuple)):
return [single_clean(item, initial) for item in data]
return single_clean(data, initial)
class UploadForm(forms.Form):
files = MultipleFileField(label="Выберите файлы")
Этот приём — рекомендованный в официальной документации Django способ обойти запрет на multiple. Без пары «свой виджет плюс своё поле» валидация будет проверять только один файл.
Проверка: python manage.py shell, затем from myapp.forms import UploadForm; UploadForm() — форма создаётся без исключений.
Шаг 4. Напишите представление, которое читает getlist
Вот ключевое место, ради которого вся статья. Файлы лежат в request.FILES, но обращение по ключу request.FILES["files"] вернёт только один — последний. Чтобы получить все, нужен getlist.
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render
from .forms import UploadForm
def upload_files(request):
saved = []
if request.method == "POST":
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
storage = FileSystemStorage()
# getlist возвращает ВСЕ файлы поля, а не последний
for uploaded in request.FILES.getlist("files"):
name = storage.save(uploaded.name, uploaded)
saved.append(storage.url(name))
else:
form = UploadForm()
return render(request, "upload.html", {"form": form, "saved": saved})
Проверка: после отправки формы список saved содержит столько URL, сколько файлов вы выбрали, а не один.
Шаг 5. Шаблон с правильным enctype
Создайте templates/upload.html. Атрибут multiple у <input> добавит наш виджет автоматически, а enctype надо прописать руками:
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Загрузить</button>
</form>
{% if saved %}
<ul>
{% for url in saved %}
<li><a href="{{ url }}">{{ url }}</a></li>
{% endfor %}
</ul>
{% endif %}
Проверка: откройте /upload/, выберите два-три файла (в диалоге выбора удерживайте Ctrl или Shift), нажмите «Загрузить». В браузере появится список ссылок, а в папке media — сами файлы.
Одна строка решает половину проблем с загрузкой:request.FILES.getlist("files"), а неrequest.FILES["files"]. Индексный доступ отдаёт последний файл и создаёт иллюзию, что «сохраняется только один».
Если не получилось
- Сохранился только один файл. Вы читаете
request.FILES["files"]вместоgetlist. Индексный доступ кMultiValueDictвозвращает последнее значение по ключу. - request.FILES пустой, файлы не приходят. В форме нет
enctype="multipart/form-data"или метод не POST. Проверьте оба. - ValueError: ClearableFileInput doesn't support uploading multiple files. Вы навесили
multipleна стандартное поле без своего виджета. Вернитесь к шагу 3 — нужныMultipleFileInputиMultipleFileField. - SuspiciousFileOperation или PermissionError при сохранении. Нет прав на запись в
MEDIA_ROOTлибо путь указан неверно. Проверьте, что папка существует и доступна на запись. - Файлы загрузились, но ссылки выдают 404. Не подключена раздача медиа из шага 2 либо
DEBUG=False. В проде отдачей должен заниматься веб-сервер, а не Django.
Что учесть до продакшена
Инструкция даёт рабочий минимум. Прежде чем открывать форму реальным пользователям, посмотрите на три вещи, которые в учебном примере опущены:
- Лимит размера. Django буферизует большие загрузки на диск после порога
FILE_UPLOAD_MAX_MEMORY_SIZE, но общий лимит на тело запроса задаёт и веб-сервер (например,client_max_body_sizeв Nginx). Настройте оба. - Одинаковые имена.
FileSystemStorage.saveсам добавит суффикс при коллизии имён, но полагаться на это в проде рискованно — лучше генерировать имя сами. - Валидация типов. Проверять расширение и MIME стоит на сервере, а не только атрибутом
acceptв HTML — его легко обойти.
Сохранение файлов в модель — отдельная тема, здесь рассматриваем только приём и запись через storage. Дальше настройте хранение: для боевого проекта чаще берут объектное хранилище (S3-совместимое) через django-storages вместо локального диска — так файлы не теряются при передеплое и масштабируются между несколькими серверами.
AI-стилист и фотограф: Замена фона, ретушь, аватарки, фото для резюме.
Спросить за 20 ₽Источники: Django docs — Uploading multiple files (File Uploads), Django docs — Managing files (MEDIA_ROOT, storage)
Частые вопросы
Что делать, если у меня Django 4.2, а не 5.x?
Почему сохранился только последний файл?
Форма отправляется, но request.FILES пустой. В чём дело?
Нужен ли Pillow для загрузки нескольких файлов?
Можно ли ограничить количество загружаемых файлов?
Как это будет работать в проде под нагрузкой?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.