REST API - обработка файлов (т.е. изображений) - лучшие практики

198

Мы разрабатываем сервер с REST API, который принимает и отвечает с помощью JSON. Проблема в том, если вам нужно загрузить изображения с клиента на сервер.

Примечание: а также я имею в виду случай использования, в котором сущность (пользователь) может иметь несколько файлов (carPhoto, licensePhoto), а также иметь другие свойства (имя, адрес электронной почты ...), но когда вы создаете нового пользователя, вы не Не отправляйте эти изображения, они добавляются после процесса регистрации.


Решения, о которых я знаю, но у каждого из них есть свои недостатки

1. Используйте multipart / form-data вместо JSON

хорошо : запросы POST и PUT настолько RESTful, насколько это возможно, они могут содержать текстовые вводы вместе с файлом.

минусы : это больше не JSON, его гораздо проще тестировать, отлаживать и т. д. по сравнению с multipart / form-data

2. Разрешить обновлять отдельные файлы

POST-запрос для создания нового пользователя не позволяет добавлять изображения (что нормально в нашем случае использования, как я уже говорил в начале), загрузка изображений выполняется с помощью PUT-запроса в виде multipart / form-data, например, в / users / 4 / carPhoto

хорошо : все (кроме самой загрузки файла) остается в JSON, его легко тестировать и отлаживать (вы можете регистрировать завершенные запросы JSON, не опасаясь их длины)

минусы : Это не интуитивно понятно, вы не можете POST или PUT все переменные объекта сразу, а также этот адрес /users/4/carPhotoможно рассматривать скорее как коллекцию (стандартный сценарий использования для REST API выглядит следующим образом /users/4/shipments). Обычно вы не можете (и не хотите) ПОЛУЧИТЬ / ПОСТАВИТЬ каждую переменную объекта, например users / 4 / name. Вы можете получить имя с помощью GET и изменить его с помощью PUT в users / 4. Если после идентификатора есть что-то, то обычно это другая коллекция, например users / 4 / reviews

3. Используйте Base64

Отправьте его в формате JSON, но закодируйте файлы с помощью Base64.

хорошо : То же, что и первое решение, это как можно RESTful сервис.

минусы : еще раз, тестирование и отладка намного хуже (тело может иметь мегабайты данных), увеличение размера, а также времени обработки как на клиенте, так и на сервере


Я действительно хотел бы использовать решение нет. 2, но у него есть свои минусы ... Кто-нибудь может дать мне лучшее представление о том, что является лучшим решением?

Моя цель состоит в том, чтобы RESTful-сервисы включали в себя как можно больше стандартов, в то время как я хочу сделать его как можно более простым.

libik
источник
Вы также можете найти это полезным: stackoverflow.com/questions/4083702/…
Маркон
5
Я знаю, что эта тема старая, но мы недавно столкнулись с этой проблемой. Лучший подход, который у нас есть, похож на ваш номер 2. Мы загружаем файлы прямо в API, а затем прикрепляем эти файлы в модель. При таком сценарии вы можете создавать загружаемые изображения до, после или на той же странице, что и форма, на самом деле не имеет значения. Хорошая дискуссия!
Тьяго Матос
2
@TiagoMatos - да, точно, я описал это в одном ответе, который я недавно принял
libik
6
Спасибо, что задали этот вопрос.
Зухайер Тахир
1
«также этот адрес / users / 4 / carPhoto можно рассматривать скорее как коллекцию» - нет, это не похоже на коллекцию и не обязательно будет рассматриваться как одна. Совершенно нормально иметь отношение к ресурсу, который является не коллекцией, а отдельным ресурсом.
B12Toaster

Ответы:

156

OP здесь (я отвечаю на этот вопрос через два года, пост, сделанный Дэниелом Сереседо, был неплохим, но веб-сервисы развиваются очень быстро)

После трех лет постоянной разработки программного обеспечения (с акцентом на архитектуру программного обеспечения, управление проектами и архитектуру микросервисов) я определенно выбрал второй путь (но с одной общей конечной точкой) в качестве лучшего.

Если у вас есть специальная конечная точка для изображений, это дает вам гораздо больше возможностей для обработки этих изображений.

У нас один и тот же REST API (Node.js) для мобильных приложений (iOS / android) и внешнего интерфейса (с использованием React). Это 2017 год, поэтому вы не хотите хранить изображения локально, вы хотите загрузить их в какое-то облачное хранилище (Google cloud, s3, cloudinary, ...), поэтому вам нужна некоторая общая обработка их.

Наш типичный процесс заключается в том, что, как только вы выбираете изображение, оно начинает загружаться в фоновом режиме (обычно это POST on / images endpoint), возвращая вам идентификатор после загрузки. Это действительно удобно для пользователя, поскольку пользователь выбирает изображение, а затем обычно переходит к некоторым другим полям (например, адрес, имя, ...), поэтому, когда он нажимает кнопку «отправить», изображение обычно уже загружено. Он не ждет и смотрит на экран с надписью «загрузка ...».

То же самое касается получения изображений. В особенности благодаря мобильным телефонам и ограниченным объемам мобильных данных вы не хотите отправлять оригинальные изображения, вы хотите отправлять изображения с измененным размером, чтобы они не занимали слишком большую полосу пропускания (а для того, чтобы сделать ваши мобильные приложения быстрее, вам часто чтобы изменить его размер, вы хотите, чтобы изображение идеально вписывалось в ваш взгляд). По этой причине хорошие приложения используют что-то вроде облачного хранилища (или у нас есть собственный сервер изображений для изменения размера).

Кроме того, если данные не являются частными, то вы отправляете обратно в app / frontend только URL-адрес и загружаете его напрямую из облачного хранилища, что значительно экономит трафик и время обработки вашего сервера. В наших больших приложениях загружается много терабайт каждый месяц, и вы не хотите обрабатывать это непосредственно на каждом из ваших REST API-серверов, которые сосредоточены на работе CRUD. Вы хотите обработать это в одном месте (наш Imageserver, который имеет кэширование и т. Д.) Или позволить облачным службам обрабатывать все это.


Минусы: Единственные «минусы», о которых вы должны подумать, это «не назначенные изображения». Пользователь выбирает изображения и продолжает заполнять другие поля, но затем он говорит «нет» и выключает приложение или вкладку, но между тем вы успешно загрузили изображение. Это означает, что вы загрузили изображение, которое нигде не назначено.

Есть несколько способов справиться с этим. Самый простой вариант - «Мне все равно», который уместен, если это происходит не очень часто или у вас даже есть желание сохранить каждое изображение, отправленное вами пользователем (по любой причине), и вы не хотите ничего удаление.

Другой вариант тоже прост - у вас есть CRON, то есть каждую неделю, и вы удаляете все неназначенные изображения старше одной недели.

libik
источник
Что произойдет, если [как только вы выберете изображение, оно начнет загружаться в фоновом режиме (обычно POST в / конечная точка изображений), возвращая вам идентификатор после загрузки], когда запрос не будет выполнен из-за подключения к Интернету? Будете ли вы предлагать пользователю, пока он будет работать с другими полями (например, адрес, имя, ...)? Могу поспорить, что вы все еще будете ждать, пока пользователь нажмет кнопку «отправить», и повторите ваш запрос, заставив его подождать, пока он смотрит на экран «выгрузка ...».
Адромиль Бале
5
@AdromilBalais - RESTful API не имеет состояния, поэтому ничего не делает (сервер не отслеживает состояние потребителя). Потребитель услуги (т. Е. Веб-страница или мобильное устройство) несет ответственность за обработку неудавшихся запросов, поэтому потребитель должен решить, вызовет ли он тот же самый запрос сразу после того, как этот отказал, или что делать (т.е. показать «Загрузка изображения не удалась - хотите повторить попытку»). ")
libik
2
Очень информативный и поучительный ответ. Спасибо за ответы.
Зухайер Тахир
Это на самом деле не решает первоначальную проблему. Это просто говорит "использовать облачный сервис"
Мартин Музатко
3
@MartinMuzatko - это так, он выбирает второй вариант и говорит, как его использовать и почему. Если вы имеете в виду «но это не идеальный вариант, который позволяет отправлять все в одном запросе и без последствий» - да, к сожалению, такого решения не существует.
libik
105

Есть несколько решений :

  1. Первое о пути к ресурсу :

    • Смоделируйте изображение как ресурс самостоятельно:

      • Вложено в пользователя (/ user /: id / image): отношения между пользователем и изображением устанавливаются неявно

      • В корневом пути (/ изображение):

        • Клиент несет ответственность за установление отношений между изображением и пользователем, или;

        • Если контекст безопасности предоставляется с запросом POST, используемым для создания изображения, сервер может неявно установить связь между аутентифицированным пользователем и изображением.

    • Вставить изображение как часть пользователя

  2. Второе решение о том, как представить ресурс изображения :

    • Как Base 64 закодировал полезную нагрузку JSON
    • В качестве составной части

Это будет мой трек решения:

  • Я обычно предпочитаю дизайн, а не производительность, если нет веских оснований для этого. Это делает систему более удобной в обслуживании и может быть легко понята интеграторам.
  • Поэтому моя первая мысль состоит в том, чтобы перейти к представлению Base64 для ресурса изображения, поскольку оно позволяет вам хранить все в формате JSON. Если вы выбрали эту опцию, вы можете смоделировать путь к ресурсу так, как вам нравится.
    • Если отношение между пользователем и изображением составляет 1 к 1, я бы предпочел смоделировать изображение как атрибут, особенно если оба набора данных обновляются одновременно. В любом другом случае вы можете свободно выбирать модель изображения в качестве атрибута, обновляя его через PUT или PATCH, или как отдельный ресурс.
  • Если вы выберете многокомпонентную полезную нагрузку, я буду вынужден смоделировать изображение как собственный ресурс, чтобы на другие ресурсы, в нашем случае на пользовательский ресурс, не повлияло решение использовать двоичное представление для изображения.

Тогда возникает вопрос: влияет ли производительность на выбор base64 против multipart? , Мы могли бы подумать, что обмен данными в многочастном формате должен быть более эффективным. Но эта статья показывает, насколько мало оба представления отличаются по размеру.

Мой выбор Base64:

  • Последовательное дизайнерское решение
  • Незначительное влияние на производительность
  • Поскольку браузеры понимают URI данных (изображения в кодировке base64), их не нужно преобразовывать, если клиент является браузером.
  • Я не буду голосовать за то, чтобы иметь его в качестве атрибута или автономного ресурса, это зависит от вашей проблемной области (которую я не знаю) и ваших личных предпочтений.
Даниэль Сереседо
источник
3
Разве мы не можем кодировать данные, используя другие протоколы сериализации, такие как protobuf и т. Д.? По сути, я пытаюсь понять, существуют ли другие более простые способы решения проблемы увеличения размера и времени обработки, связанные с кодировкой base64.
Энди Дафрен
1
Очень интересный ответ. спасибо за пошаговый подход. Это заставило меня понять ваши идеи намного лучше.
Зухайер Тахир
13

Ваше второе решение, пожалуй, самое правильное. Вы должны использовать спецификации HTTP и mimetypes так, как они были предназначены, и загружать файл через multipart/form-data. Что касается обработки отношений, я бы использовал этот процесс (помните, что я ничего не знаю о ваших предположениях или дизайне системы):

  1. POSTчтобы /usersсоздать пользовательский объект.
  2. POSTизображение /images, чтобы убедиться, что вы возвращаете Locationзаголовок, где изображение может быть получено в соответствии со спецификацией HTTP.
  3. PATCHчтобы /users/carPhotoи присвоить ему идентификатор фотографии , приведенной в Locationзаголовке шага 2.
mmcclannahan
источник
1
У меня нет прямого контроля над тем, «как клиент будет использовать API» ... Проблема в том, что «мертвые» картинки, которые не исправлены на некоторых ресурсах ...
libik
4
Обычно, когда вы выбираете второй вариант, предпочтительнее загружать сначала медиа-элемент и возвращать медиа-идентификатор клиенту, тогда клиент может отправлять данные объекта, включая медиа-идентификатор, при таком подходе избегаются поврежденные объекты или несоответствие информации.
Келлерман Риверо
2

Там нет простого решения. У каждого способа есть свои плюсы и минусы. Но канонический способ использовать первый вариант: multipart/form-data. Как говорится в руководстве по рекомендациям W3

Тип контента "multipart / form-data" должен использоваться для отправки форм, которые содержат файлы, данные не ASCII и двоичные данные.

На самом деле мы не отправляем формы, но все еще применяется неявный принцип. Использование base64 в качестве двоичного представления некорректно, потому что вы используете неправильный инструмент для достижения своей цели, с другой стороны, второй вариант заставляет ваших клиентов API выполнять больше работы, чтобы использовать вашу службу API. Вы должны выполнить тяжелую работу на стороне сервера, чтобы предоставить простой в использовании API. Первый вариант нелегко отладить, но когда вы это делаете, он, вероятно, никогда не изменится.

Использование multipart/form-dataТы наклеить с REST / HTTP философии. Вы можете посмотреть ответ на подобный вопрос здесь .

Другой вариант, если смешивать альтернативы, вы можете использовать multipart / form-data, но вместо того, чтобы отправлять каждое значение отдельно, вы можете отправить значение с именем payload с полезной нагрузкой json внутри. (Я попробовал этот подход с использованием ASP.NET WebAPI 2 и работает нормально).

Келлерман Риверо
источник
2
Это руководство по рекомендациям W3 здесь неуместно, так как оно находится в контексте спецификации HTML 4.
Иоганн
1
Совершенно верно .... "не ASCII-данные" требует multipart? В двадцать первом веке? В мире UTF-8? Конечно, это смешная рекомендация на сегодня. Я даже удивлен, что существовал в HTML 4 дня, но иногда мир интернет-инфраструктуры движется очень медленно.
Рэй Тоал