Для работы с изображениями и другими файлами во фреймворке Python Django существует готовый механизм реализуемый через модели. Процесс хранения картинок и файлов в базах данных может вызвать путаницу если вы не имели особого опыта работы с ними. Процесс создания возможности загрузки файлов состоит из нескольких шагов, которые мы рассмотрим ниже на примерах.
Подготовка тестового проекта
Создадим проект, на котором будут показаны следующие примеры. У вас уже должен быть установлен Django и, по вашему желанию, создана виртуальная среда. В примерах будет использоваться название проекта 'file_project', вы можете выбрать имя на свое усмотрение:
# создаем проект
django-admin startproject file_project
cd file_project
# создадим приложение
python manage.py startapp upload_app
Мы должны добавить наше приложение в конфигурацию проекта через 'settings.py'. В 'INSTALLED_APPS' нужно добавить следующую строку:
# file_project/settings.py
'upload_app.apps.UploadAppConfig',
Выполните миграцию:
python manage.py migrate
Создание модели для загрузки файлов
Каждая модель отображает какую-то таблицу в базе данных. В этих таблицах, а именно колонках, хранятся определенные типы данных (строки, числа и т.д.). Один из таких типов данных - файлы. Однако хранить файлы в SQL базах, в большинстве случаев, считается плохой практикой. Одна из причин не делать этого заключается в непригодности SQL баз в поиске по таким типам данных.
В большинстве случаев хранение файлов организуется в несколько шагов:
- Ваше приложение знает полный путь до изображения;
- В базу данных добавляется путь до изображения, в виде строки, например '/media/image1.jpg';
- Ваше приложение физически сохраняет изображения используя указанный путь.
Для такой реализации в Django есть два типа полей (Fields):
- FileField - для любых типов данных;
- ImageField - наследует все методы FileField, добавляет валидацию картинок и методы. Например мы можем получить размер картинки используя image.width или image.height.
Модели создаются в 'models.py' вашего проекта. Добавьте в него следующую модель:
# upload_app/models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=150)
cover = models.ImageField(upload_to='images/')
book = models.FileField(upload_to='books/')
def __str__(self):
return self.title
uppload_to - это папка, в которую будут сохранены файлы этого поля. Этот путь относительный и он продолжает путь указанный в MEDIA_ROOT (будет создана ниже). Т.е. если в upload_to указан 'images/', то полный путь до файла будет '/media_root/images'. Параметр upload_to - не обязателен.
В ImageField добавлена проверка, что файл имеет тип изображения. Так же у ImageField есть методы возвращающие высоту и ширину. Ширину и высоту так же можно сохранить в отдельные модели используя параметры height_field и width_field. Часть этих возможностей по работе с изображениями выполняется через библиотеку Pillow и поэтому ее нужно установить:
pip install pillow
Удаление любого объекта удаляет только запись из базы. На файловой системе файл остается.
После создания моделей выполните миграции:
python manage.py makemigrations
python manage.py migrate
Динамический upload_to
Папки для сохранения данных можно организовать динамически. Например мы можем сделать так, что бы файл сохранялся в каталог с названием сегодняшней даты формата год-месяц-день:
upload_to='books/%Y-%m-%d/'
Этот способ соответствует методу strftime() из библиотеки datetime.
Так же можно создать путь соответствующий пользователю, который выполняет загрузку:
def user_directory_path(instance, filename):
# путь, куда будет осуществлена загрузка MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class MyModel(models.Model):
...
upload = models.FileField(upload_to=user_directory_path)
Папка соответствующая пользователю будет создана только в том случае, если модель MyModel будет иметь поле с названием user. Т.е. instance - это и есть сам объект модели. Пример того как это можно сделать:
Использование существующих файлов и папок с FilePathField
Во фреймворке Python Django есть так же поле FilePathField. Его основная задача - создание записи в базе на основании существующего файла.
При определении такого поля Django сканирует указанный путь получая файлы из него и предлагает вам выбор. После выбора файла - путь до него сохраняется в базе:
class Foo(models.Model):
# Указывает на абсолютный путь
audio = models.FilePathField(path='/home/user/')
Доступны поля для рекурсивного поиска и по маске. Path - это абсолютный путь (в отличие от предыдущих примеров).
В панели администрирования, если указанная папка не пуста, файл будет выводиться следующим образом:
FilePathField не следует использовать в директориях с вашим приложением т.к. приводит к уязвимостям.
Используем CRUD запросы в Django 3 на примере приложения
Переменная MEDIA_ROOT и MEDIA_URL
Для хранения всех загруженных файлов определяется отдельная папка. Эта папка указывается в переменной MEDIA_ROOT, в файле 'settings.py'. Есть еще переменная MEDIA_URL, которая используется в шаблонах и адресах для обращения к файлам в MEDIA_ROOT.
Эти настройки прописываются в файле 'settings.py'. Для Django 3+ используются следующие значения:
# file_project/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
Для Django до 3-ей версии это выглядело так:
# file_project/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
BASE_DIR, в обоих случаях, это путь до вашего проекта. В примерах выше, в этой папке, вы должны создать папку 'media' самостоятельно:
Теперь, например, сохранять файлы вы будете по следующему пути:
'/var/www/file_project/media/images/1.jpg'
А открывать их будет по следующей ссылке:
'http://localhost/file_project/media/images/1.jpg'
В Django так же есть похожая переменная - STATIC_ROOT и STATIC_URL. Эти переменные указывают на папки в которые вы сами загружали файлы не используя Django. Хоть вы можете соединить STATIC_ROOT и MEDIA_ROOT - это не рекомендуется делать т.к. приведет к уязвимости. Совмещения этих путей стоит избегать.
Панель администрирования
Зарегистрируем наши модели что бы они отображались в панели администрирования. Для этого отредактируем файл 'admin.py':
# upload_app/admin.py
from django.contrib import admin
from .models import Book
admin.site.register(Book)
После этого создайте супер пользователя и запустите сервер:
python manage.py createsuperuser
python manage.py runserver
Зайдя по адресу 'http://127.0.0.1:8000/admin/' и авторизовавшись можно увидеть созданную модель и поля для загрузки файлов:
Если открыть директорию прописанную в "MEDIA_ROOT" можно увидеть загруженные файлы (могут отличаться из-за настроек):
Загрузка и вывод файлов
Что бы вывести изображение на странице нужно выполнить следующие шаги:
- Создать url связывающий запрос пользователя с какой-то логикой;
- Шаблон HTML, который преобразует данные и вернет их пользователю;
- Создать функцию или класс, который свяжет url и шаблон.
Реализуем эти пункты
Создание ссылки в urls.py
В вебе есть понятие статических файлов - это любой файл который не изменяясь возвращается пользователю: картинки, видео, файлы css, js и т.д. По умолчанию Django не занимается обработкой таких файлов. Обычно, запросы к статическим файлам обрабатывает другая программа, например веб сервер Nginx.
В рамках разработки, в Django, предусмотрена возможность возвращать пользователю статические файлы. Добавим в файл 'url.py' ссылку по которой будет возвращаться страница с картинкой и возможность обработки загруженных файлов:
# file_project/url.py
from django.contrib import admin
from django.urls import path
from django.conf.urls.static import static
from django.conf import settings
# функция для возврата картинки
from upload_app.views import home_page
urlpatterns = [
path('admin/', admin.site.urls),
# Url и функция, которая вернет картинку
path('', home_page)
]
# включаем возможность обработки картинок
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Возможность возвращать пользователю статические файлы так же связана с настройкой DEBUG в 'settings.py'. Файлы будут нормально обрабатываться, если DEBUG=True.
Вывод изображения на странице
Что бы вывести файл на странице достаточно обратить к нужному значению из базы. То есть для вывода картинки достаточно следующей функции в 'upload_app/views.py':
from django.shortcuts import render
from .models import Book
def home_page(request):
# получаем все значения модели
data = Book.objects.all()
return render(request, 'home_page.html', {'data': data})
В созданной модели у меня есть 3 поля:
- title - строка;
- cover - картинка;
- book - файл.
Что бы вывести их на странице я создам следующий шаблон по пути:
upload_app/templates/home_page.html
{% for b in data %}
<a href="{{ b.book.url }}">{{ b.title }}</a>
<img src="{{ b.cover.url }}">
{% endfor %}
После запуска сервера можно будет увидеть следующий результат (при наличии данных в базе):
Загрузка документа со страницы и его сохранение
Каждый раз, когда пользователь отправляет со страницы в Django какие-то файлы, они помещаются в массив (похожий на словарь). Этот массив доступен через переменную 'request.FILES'. Ключи в этом массиве соответствуют тегу name в html форме. Значения этих ключей - объект типа 'InMemoryUploadedFile', хранящий в себе имя файла, его данные и т.д.
Что бы сохранить файл с Django мы должны использовать следующую функцию:
# upload_app/views.py
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def home_page(request):
# POST - обязательный метод
if request.method == 'POST' and request.FILES:
# получаем загруженный файл
file = request.FILES['myfile1']
fs = FileSystemStorage()
# сохраняем на файловой системе
filename = fs.save(file.name, file)
# получение адреса по которому лежит файл
file_url = fs.url(filename)
return render(request, 'home_page.html', {
'file_url': file_url
})
return render(request, 'home_page.html')
В шаблоне 'upload_app/templates/home_pahe.html' мы должны создать форму с атрибутом enctype="multipart/form-data". Если этого атрибута не будет - 'request.FILES' будет пустым:
<!-- Метод POST и установлен enctype -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- myfile1 - переменная передающееся в Django -->
<input type="file" name="myfile1">
<button type="submit">Загрузка</button>
</form>
<p>Ссылка: <a href="{{ file_url }}">{{ file_url }}</a></p>
Теперь, запустив сервер и, открыв главную страницу, у нас будет возможность загрузки файла в корень MEDIA_ROOT:
В примере выше файл в базу не сохраняется. Что бы путь до файла сохранился в базу мы должны передать ему объект file. Ниже пример, если бы вы загружали несколько файлов сразу:
Book.objects.create(title='some title', cover=request.FILES['myfile1'], book=request.FILES['myfile2'])
Создание формы
Все шаги, описанные выше, можно сократить использовав существующие возможности Django. Например в Django есть формы, которые можно создать на основе существующей модели. Такие формы упрощают процесс валидации (соответствует ли тип данных Django типу в базе), представления в шаблоне (HTML теги создаются сами) и много другое.
Формы, обычно, создаются в отдельном файле 'forms.py':
upload_app/forms.py
Для работы формы нужна модель, на основе которой можно определить поля. Поместим в этот файл следующий код:
from .models import Book
from django.forms import ModelForm
class BookForm(ModelForm):
class Meta:
# Название модели на основе
# которой создается форма
model = Book
# Включаем все поля с модели в форму
fields = '__all__'
Для работы с пользователем, в случае создания объектов, в Django есть отдельный класс - CreateView. Этот класс сам определит тип запроса ('GET' или 'POST'), проведет валидацию отправленных полей с помощью формы и сохранит значение в базе. Создадим такой класс:
# upload_app/views.py
from django.views.generic import CreateView
from .forms import BookForm
class BookCreate(CreateView):
# Модель куда выполняется сохранение
model = Book
# Класс на основе которого будет валидация полей
form_class = BookForm
# Выведем все существующие записи на странице
extra_context = {'books': Book.objects.all()}
# Шаблон с помощью которого
# будут выводиться данные
template_name = 'book_create.html'
# На какую страницу будет перенаправление
# в случае успешного сохранения формы
success_url = '/book/'
В файл маршрутизации добавим маршрут, который свяжет url с новым классом:
# file_project/urls.py
...
from upload_app.views import home_page, BookCreate
urlpatterns = [
path('admin/', admin.site.urls),
path('', home_page),
path('book/', BookCreate.as_view())
]
...
Создадим файл шаблона по пути:
upload_app/templates/book_create.html
В этот файл добавим форму и вывод существующих записей:
<!-- Устанавливаем multipart/form-data -->
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<!-- Выводим каждое поле формы в отдельном параграфе -->
{{ form.as_p }}
<button type="submit">Загрузка</button>
</form>
<!-- Выводим уже созданные записи -->
{% for b in books %}
<p>{{ b.title }}</p>
<img src="{{ b.cover.url }}" width="100px">
<!-- Выводим размер картинки -->
<p>Оригинальный размер картинки {{ b.cover.width }}x{{ b.cover.height }}</p>
<br/>
<a href="{{ b.book }}">Книга</a>
{% endfor %}
После запуска сервера эта страница будет выглядеть так:
...