Загрузка файлов
Исходный кодОдна из распространённых задач веб-приложений — загрузка файлов. Это могут быть изображения, видео, PDF-файлы или файлы любого другого типа. Чтобы загружать файлы через интерфейс HTML, нам понадобится тег ввода file в форме multipart.
Ищете руководство по загрузке файлов в LiveView?
В этом руководстве описана загрузка файлов через HTTP с использованием multipart и Plug.Upload. Дополнительные сведения о загрузке файлов в LiveView, в том числе о прямой загрузке во внешнее облачное хранилище со стороны клиента, см. в руководстве по загрузке файлов в LiveView.
Plug предоставляет структуру Plug.Upload для хранения данных из поля ввода file. Если пользователь выбрал файл при отправке формы, структура Plug.Upload автоматически появится в параметрах запроса.
В этом руководстве вы выполните следующие действия:
Настроите форму multipart
Добавите в форму элемент ввода файла
Проверите параметры загрузки
Управите загруженными файлами
В руководстве по Contexts guide мы сгенерировали HTML-ресурс для товаров. Чтобы показать, как работает загрузка файлов в Phoenix, можно повторно использовать созданную там форму. Обратитесь к этому руководству за инструкциями по созданию ресурса товара, который вы будете использовать здесь.
Настройка формы multipart
Первое, что нужно сделать, — преобразовать форму в форму multipart. Компонент HelloWeb.CoreComponents form/1 принимает атрибут multipart, с помощью которого можно это указать.
Вот форма из lib/hello_web/controllers/product_html/product_form.html.heex с внесённым изменением:
<.form :let={f} for={@changeset} action={@action} multipart>
...
Добавление поля ввода файла
После создания формы multipart понадобится поле ввода file. Вот как это сделать, также в product_form.html.heex:
...
<.input field={f[:photo]} type="file" label="Photo" />
<.button>Save Product</.button>
</.form>
После отображения компонента HelloWeb.CoreComponents input/1 по умолчанию получится следующий HTML:
<div> <label for="product_photo" class="block text-sm...">Photo</label> <input type="file" name="product[photo]" id="product_photo" class="mt-2 block w-full..."> </div>
Обратите внимание на атрибут name поля ввода file. Это создаст ключ "photo" в карте product_params, который будет доступен в действии контроллера.
На этом настройка формы завершена. Когда пользователи отправят форму, запрос POST будет направлен в действие HelloWeb.ProductController create/2.
Нужно ли добавлять поле photo в схему Ecto?
Поле ввода photo не обязательно должно быть частью схемы, чтобы попасть в product_params. Однако если вы хотите сохранить какие-либо свойства фотографии в базе данных, их нужно добавить в схему Hello.Product.
Проверка параметров загрузки
Поскольку вы сгенерировали HTML-ресурс, теперь можно запустить сервер с помощью mix phx.server, открыть страницу http://localhost:4000/products/new и создать новый товар с фотографией.
Прежде чем продолжить, добавьте IO.inspect product_params в начало действия ProductController.create/2 в lib/hello_web/controllers/product_controller.ex. Это выведет product_params в журнал разработки, чтобы вы могли лучше понять происходящее.
...
def create(conn, %{"product" => product_params}) do
IO.inspect product_params
...
После этого product_params выведет в журнал следующее:
%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0",
"photo" => %Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}}
У вас есть ключ "photo", которому соответствует предварительно заполненная структура Plug.Upload, представляющая загруженную фотографию.
Чтобы упростить чтение, сосредоточимся на самой структуре:
%Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}
Plug.Upload содержит тип содержимого файла, исходное имя файла и путь к временному файлу, созданному для вас Plug. В данном случае "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/" — это каталог, созданный Plug для хранения загруженных файлов. Каталог сохраняется между запросами. "multipart-558399-917557-1" — это имя, которое Plug присвоил загруженному файлу. Если бы у вас было несколько полей ввода file и пользователь выбрал фотографии для каждого из них, файлы оказались бы в разных временных каталогах. Plug позаботится о том, чтобы имена всех файлов были уникальными.
Файлы Plug.Upload являются временными
Plug удаляет загруженные файлы из своего каталога по завершении запроса. Если вам нужно что-то сделать с файлом, сделайте это до этого момента (или передайте его, но это выходит за рамки данного руководства).
Управление загруженными файлами
Получив доступ к структуре Plug.Upload в контроллере, вы можете выполнять с ней любые операции. Например, можно сделать одно или несколько из следующих действий:
Проверить наличие файла с помощью
File.exists?/1Скопировать файл в другое место файловой системы с помощью
File.cp/2Передать файл другому процессу Elixir с помощью
Plug.Upload.give_away/3Отправить его в S3 с помощью внешней библиотеки
Отправить файл обратно клиенту с помощью
Plug.Conn.send_file/5
В рабочей системе может понадобиться скопировать файл в корневой каталог, например /media. В этом случае важно обеспечить уникальность имён файлов. Например, если пользователям разрешено загружать обложки товаров, для создания уникального имени можно использовать идентификатор товара:
if upload = product_params["photo"] do
extension = Path.extname(upload.filename)
File.cp(upload.path, "/media/#{product.id}-cover#{extension}")
end
Затем в lib/my_app_web/endpoint.ex можно добавить plug Plug.Static, чтобы файлы были доступны по адресу "/media":
plug Plug.Static, at: "/uploads", from: "/media"
Теперь загруженный файл можно открыть в браузере по пути, например "/uploads/1-cover.jpg". На практике при загрузке файлов нужно учитывать и другие аспекты, например проверку расширений, кодирование имён и так далее. Часто предпочтительнее использовать библиотеку, которая уже решает такие задачи.
Наконец, обратите внимание: если в поле ввода file нет данных, вы не получите ни ключ "photo", ни структуру Plug.Upload. Вот product_params из журнала.
%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0"}
Настройка ограничений загрузки
Преобразование данных, отправленных формой, в структуру Plug.Upload выполняет plug Plug.Parsers, который можно найти в HelloWeb.Endpoint:
# lib/hello_web/endpoint.ex plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library()
Помимо указанных выше параметров, Plug.Parsers принимает и другие параметры для управления загрузкой данных:
-
:length— задаёт максимальную длину тела запроса для чтения; по умолчанию8_000_000байт -
:read_length— задаёт количество байт, считываемых за один раз; по умолчанию1_000_000байт -
:read_timeout— задаёт время ожидания получения каждого фрагмента; по умолчанию15_000мс
Первый параметр задаёт максимальный допустимый объём данных. Остальные параметры определяют, какой объём данных мы ожидаем прочитать и с какой частотой. Если клиент не может передавать данные достаточно быстро, соединение будет разорвано. В Phoenix заданы разумные значения по умолчанию, но при особых обстоятельствах их может понадобиться изменить, например если очень медленные клиенты должны отправлять большие фрагменты данных.
Стоит также отметить, что эти ограничения важны как механизм безопасности. Например, если не установить ограничение на загрузку данных, злоумышленники смогут открыть тысячи соединений с приложением и отправлять по одному байту каждые 2 минуты. Передача займёт очень много времени, при этом будут заняты все соединения с сервером. Указанные выше ограничения требуют разумной скорости передачи данных и тем самым несколько усложняют жизнь злоумышленникам.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/file_uploads.html