REST API - przetwarzanie plików (tj. Obrazów) - najlepsze praktyki

198

Rozwijamy serwer z interfejsem API REST, który przyjmuje i odpowiada za pomocą JSON. Problem polega na tym, że jeśli chcesz przesłać obrazy z klienta na serwer.

Uwaga: mówię również o przypadku użycia, w którym podmiot (użytkownik) może mieć wiele plików (carPhoto, licensePhoto), a także mieć inne właściwości (imię i nazwisko, adres e-mail ...), ale kiedy tworzysz nowego użytkownika, nie nie wysyłają tych zdjęć, są one dodawane po procesie rejestracji.


Znane mi rozwiązania, ale każde z nich ma pewne wady

1. Użyj danych wieloczęściowych / formularzy zamiast JSON

dobrze : Żądania POST i PUT są tak RESTful jak to możliwe, mogą zawierać dane tekstowe wraz z plikiem.

Wady : To już nie jest JSON, który jest znacznie łatwiej testować, debugować itp. w porównaniu do danych wieloczęściowych / formularzy

2. Pozwól zaktualizować osobne pliki

Żądanie POST dotyczące utworzenia nowego użytkownika nie pozwala na dodawanie zdjęć (co jest ok w naszym przypadku użycia, jak powiedziałem na początku), przesyłanie zdjęć odbywa się za pomocą żądania PUT jako multipart / form-data na przykład / users / 4 / carPhoto

dobrze : wszystko (oprócz samego przesyłania pliku) pozostaje w JSON, jest łatwe do testowania i debugowania (możesz rejestrować pełne żądania JSON bez obawy o ich długość)

Wady : nie jest intuicyjne, nie można jednocześnie POST lub PUT wszystkich zmiennych bytu, a także ten adres /users/4/carPhotomożna traktować bardziej jako zbiór (wygląda to tak jak w przypadku standardowego interfejsu API REST /users/4/shipments). Zwykle nie możesz (i nie chcesz) GET / PUT każdej zmiennej bytu, na przykład users / 4 / name. Możesz uzyskać nazwę za pomocą GET i zmienić ją za pomocą PUT na users / 4. Jeśli po identyfikatorze jest coś, zwykle jest to inna kolekcja, na przykład users / 4 / reviews

3. Użyj Base64

Wyślij jako JSON, ale koduj pliki za pomocą Base64.

dobrze : Tak samo jak pierwsze rozwiązanie, jest to usługa RESTful, jak to możliwe.

Wady : Po raz kolejny testowanie i debugowanie jest znacznie gorsze (ciało może mieć megabajty danych), zwiększa się rozmiar, a także czas przetwarzania zarówno w kliencie, jak i serwerze


Naprawdę chciałbym skorzystać z rozwiązania no. 2, ale ma swoje wady ... Czy ktoś może lepiej zrozumieć rozwiązanie „co jest najlepsze”?

Moim celem jest udostępnienie usług RESTful przy możliwie jak największej liczbie standardów, a ja chcę, aby było to tak proste, jak to możliwe.

libik
źródło
Może ci się to też przydać: stackoverflow.com/questions/4083702/…
Markon,
5
Wiem, że ten temat jest stary, ale ostatnio mieliśmy do czynienia z tym problemem. Najlepsze podejście, jakie mamy, jest podobne do twojego nr 2. Przesyłamy pliki bezpośrednio do interfejsu API, a następnie dołączamy je do modelu. W tym scenariuszu możesz tworzyć przesyłane obrazy przed, po lub na tej samej stronie co formularz, tak naprawdę nie ma znaczenia. Dobra dyskusja!
Tiago Matos
2
@TiagoMatos - tak, dokładnie opisałem to w jednej odpowiedzi, którą niedawno zaakceptowałem
libik
6
Dzięki, że zadałeś to pytanie.
Zuhayer Tahir
1
„również ten adres / users / 4 / carPhoto można uznać bardziej za kolekcję” - nie, nie wygląda on jak kolekcja i niekoniecznie byłby uważany za jeden. Relacja z zasobem, który nie jest kolekcją, ale pojedynczym zasobem, jest całkowicie w porządku.
B12Toaster

Odpowiedzi:

156

OP tutaj (odpowiadam na to pytanie po dwóch latach, post napisany przez Daniela Cerecedo nie był wcale zły, ale serwisy internetowe rozwijają się bardzo szybko)

Po trzech latach rozwoju oprogramowania w pełnym wymiarze godzin (koncentrując się również na architekturze oprogramowania, zarządzaniu projektami i architekturze mikrousług) zdecydowanie wybrałem drugi sposób (ale z jednym ogólnym punktem końcowym) jako najlepszy.

Jeśli masz specjalny punkt końcowy dla obrazów, daje to o wiele więcej możliwości zarządzania nimi.

Mamy ten sam interfejs API REST (Node.js) zarówno dla aplikacji mobilnych (iOS / Android), jak i interfejsu użytkownika (przy użyciu React). Jest rok 2017, dlatego nie chcesz przechowywać zdjęć lokalnie, chcesz przesłać je do niektórych pamięci w chmurze (Google Cloud, S3, Cloudinary, ...), dlatego chcesz trochę ogólnej obsługi nad nimi.

Nasz typowy przepływ polega na tym, że jak tylko wybierzesz obraz, zaczyna on przesyłać w tle (zwykle POST na / obrazach końcowych), zwracając ci identyfikator po przesłaniu. Jest to bardzo przyjazne dla użytkownika, ponieważ użytkownik wybiera obraz, a następnie zwykle przechodzi do innych pól (tj. Adres, nazwa, ...), dlatego gdy naciśnie przycisk „wyślij”, obraz jest zwykle już przesłany. Nie czeka i patrzy na ekran z napisem „przesyłanie ...”.

To samo dotyczy uzyskiwania zdjęć. Zwłaszcza dzięki telefonom komórkowym i ograniczonej ilości danych mobilnych nie chcesz wysyłać oryginalnych zdjęć, chcesz przesyłać obrazy o zmienionym rozmiarze, więc nie zajmują one tak dużej przepustowości (a aby Twoje aplikacje mobilne działały szybciej, często nie chcesz aby w ogóle go zmienić, chcesz obraz idealnie pasujący do twojego widoku). Z tego powodu dobre aplikacje używają czegoś takiego jak cloudinary (lub mamy własny serwer obrazów do zmiany rozmiaru).

Ponadto, jeśli dane nie są prywatne, odsyłasz do adresu URL aplikacji / interfejsu użytkownika, który pobiera go bezpośrednio z pamięci w chmurze, co jest ogromną oszczędnością przepustowości i czasu przetwarzania dla twojego serwera. W naszych większych aplikacjach pobieranych jest co miesiąc wiele terabajtów, nie chcesz obsługiwać tego bezpośrednio na każdym serwerze REST API, który koncentruje się na działaniu CRUD. Chcesz sobie z tym poradzić w jednym miejscu (nasz Imageserver, który ma buforowanie itp.) Lub pozwolić usługom w chmurze obsłużyć to wszystko.


Wady: Jedyne „wady”, o których powinieneś pomyśleć, to „nieprzypisane obrazy”. Użytkownik wybiera zdjęcia i kontynuuje wypełnianie innych pól, ale potem mówi „nie” i wyłącza aplikację lub kartę, ale w międzyczasie udało się załadować obraz. Oznacza to, że przesłałeś zdjęcie, które nie jest nigdzie przypisane.

Istnieje kilka sposobów radzenia sobie z tym. Najłatwiejszym z nich jest „nie dbam”, co jest istotne, jeśli nie dzieje się to zbyt często lub nawet chcesz przechowywać każdego wysłanego przez użytkownika obrazu (z dowolnego powodu) i nie chcesz żadnego usunięcie.

Kolejny jest również łatwy - masz CRON, tj. Co tydzień i usuwasz wszystkie nieprzypisane zdjęcia starsze niż tydzień.

libik
źródło
Co się stanie, jeśli [gdy tylko wybierzesz obraz, zacznie on przesyłać w tle (zwykle POST na / obrazach końcowych), zwracając ci identyfikator po przesłaniu], gdy żądanie nie powiodło się z powodu połączenia internetowego? Czy poprosisz użytkownika, aby kontynuował wypełnianie innych pól (np. Adres, nazwa, ...)? Założę się, że nadal będziesz czekać, aż użytkownik naciśnie przycisk „wyślij” i ponów prośbę, sprawiając, że zaczeka, oglądając ekran z komunikatem „przesyłanie ...”.
Adromil Balais
5
@AdromilBalais - API RESTful jest bezstanowe, dlatego nic nie robi (serwer nie śledzi stanu konsumenta). Konsument usługi (tj. Strona internetowa lub urządzenie mobilne) jest odpowiedzialny za obsługę nieudanych żądań, dlatego konsument musi zdecydować, czy wywoła natychmiast to samo żądanie po tym niepowodzeniu lub co zrobić (tj. Pokazać komunikat „Nieudane przesyłanie obrazu - spróbuj ponownie ")
libik
2
Bardzo pouczająca i pouczająca odpowiedź. Dzięki za odpowiedź.
Zuhayer Tahir
To tak naprawdę nie rozwiązuje początkowego problemu. To tylko mówi „skorzystaj z usługi w chmurze”
Martin Muzatko
3
@MartinMuzatko - robi, wybiera drugą opcję i mówi, jak należy z niej korzystać i dlaczego. Jeśli masz na myśli „ale nie jest to idealna opcja, która pozwala wysłać wszystko w jednym żądaniu i bez implikacji” - tak, niestety nie ma takiego rozwiązania.
libik
104

Należy podjąć kilka decyzji :

  1. Pierwsza o ścieżce zasobów :

    • Sam modeluj obraz jako zasób:

      • Zagnieżdżone w użytkowniku (/ user /: id / image): relacja między użytkownikiem a obrazem jest niejawna

      • W ścieżce katalogu głównego (/ image):

        • Klient ponosi odpowiedzialność za ustanowienie relacji między obrazem a użytkownikiem, lub;

        • Jeśli kontekst bezpieczeństwa jest dostarczany z żądaniem POST użytym do utworzenia obrazu, serwer może pośrednio ustanowić relację między uwierzytelnionym użytkownikiem a obrazem.

    • Osadź obraz jako część użytkownika

  2. Druga decyzja dotyczy sposobu reprezentowania zasobu obrazu :

    • Jako ładunek JSON zakodowany w standardzie Base 64
    • Jako ładunek wieloczęściowy

To byłaby moja ścieżka decyzyjna:

  • Zwykle wolę projektowanie niż wydajność, chyba że przemawiają za tym mocne argumenty. To sprawia, że ​​system jest łatwiejszy w utrzymaniu i może być łatwiej zrozumiany przez integratorów.
  • Tak więc moją pierwszą myślą było wybranie reprezentacji zasobów obrazu w formacie Base64, ponieważ pozwala zachować wszystko JSON. Jeśli wybierzesz tę opcję, możesz modelować ścieżkę zasobów według własnych upodobań.
    • Jeśli relacja między użytkownikiem a obrazem wynosi 1 do 1, wolałbym modelować obraz jako atrybut specjalnie, jeśli oba zestawy danych są aktualizowane jednocześnie. W każdym innym przypadku możesz dowolnie modelować obraz albo jako atrybut, aktualizując go za pomocą PUT lub PATCH, albo jako osobny zasób.
  • Jeśli wybierzesz ładunek wieloczęściowy, czułbym się zmuszony do modelowania obrazu, ponieważ zasób jest własny, tak więc decyzja o zastosowaniu reprezentacji binarnej dla obrazu nie ma wpływu na inne zasoby, w naszym przypadku zasób użytkownika.

Potem pojawia się pytanie: czy ma jakiś wpływ na wydajność przy wyborze base64 vs multipart? . Można by pomyśleć, że wymiana danych w formacie wieloczęściowym powinna być bardziej wydajna. Ale ten artykuł pokazuje, jak niewiele różnią się obie reprezentacje pod względem wielkości.

Mój wybór Base64:

  • Spójna decyzja projektowa
  • Niewielki wpływ na wydajność
  • Ponieważ przeglądarki rozumieją identyfikatory URI danych (obrazy zakodowane w standardzie base64), nie ma potrzeby ich przekształcania, jeśli klient jest przeglądarką
  • Nie będę głosować, czy ma to być atrybut, czy samodzielny zasób, zależy to od domeny problemu (której nie znam) i osobistych preferencji.
Daniel Cerecedo
źródło
3
Czy nie możemy kodować danych przy użyciu innych protokołów serializacji, takich jak protobuf itp.? Zasadniczo staram się zrozumieć, czy istnieją inne prostsze sposoby rozwiązania problemu zwiększenia rozmiaru i czasu przetwarzania, które jest związane z kodowaniem base64.
Andy Dufresne
1
Bardzo wciągająca odpowiedź. dzięki za podejście krok po kroku. To pozwoliło mi lepiej zrozumieć twoje punkty.
Zuhayer Tahir
13

Twoje drugie rozwiązanie jest prawdopodobnie najbardziej poprawne. Powinieneś użyć specyfikacji HTTP i typów mimetycznych w sposób, w jaki były zamierzone, i przesłać plik przez multipart/form-data. Jeśli chodzi o obsługę relacji, skorzystałbym z tego procesu (pamiętając, że znam zero na temat twoich założeń lub projektu systemu):

  1. POSTaby /usersutworzyć encję użytkownika.
  2. POSTobraz do /images, zwracając Locationnagłówek do miejsca, w którym można pobrać obraz zgodnie ze specyfikacją HTTP.
  3. PATCHdo /users/carPhotoi przypisać mu identyfikator danego zdjęcie w Locationnagłówku kroku 2.
mmcclannahan
źródło
1
Nie mam bezpośredniej kontroli nad tym, „jak klient będzie korzystał z interfejsu API” ... Problem polega na tym, że „martwe” obrazy, które nie są
załatane
4
Zwykle po wybraniu drugiej opcji preferowane jest przesłanie najpierw elementu medialnego i zwrócenie klientowi identyfikatora multimediów, a następnie klient może wysłać dane encji, w tym identyfikator medialny, takie podejście pozwala uniknąć informacji o niezgodności jednostek niezgodnych.
Kellerman Rivero
2

Nie ma łatwego rozwiązania. Każda droga ma swoje zalety i wady. Ale kanoniczny sposób korzysta pierwszą opcję: multipart/form-data. Jak mówi przewodnik rekomendacji W3

Do przesyłania formularzy zawierających pliki, dane inne niż ASCII i dane binarne należy używać typu treści „multipart / form-data”.

Tak naprawdę nie wysyłamy formularzy, ale zasada domyślna nadal obowiązuje. Używanie base64 jako reprezentacji binarnej jest niepoprawne, ponieważ używasz niewłaściwego narzędzia do osiągnięcia celu, z drugiej strony druga opcja zmusza klientów API do wykonania większej ilości zadań w celu wykorzystania usługi API. Powinieneś ciężko pracować po stronie serwera, aby dostarczyć łatwego w obsłudze interfejsu API. Pierwsza opcja nie jest łatwa do debugowania, ale kiedy to zrobisz, prawdopodobnie nigdy się nie zmienia.

Korzystając z programu, multipart/form-datatrzymasz się filozofii REST / http. Można zobaczyć odpowiedzi na podobne pytanie tutaj .

Inną opcją jest mieszanie alternatyw, można użyć danych wieloczęściowych / formularzy, ale zamiast wysyłać każdą wartość osobno, można wysłać wartość o nazwie ładunek z ładunkiem json w środku. (Próbowałem tego podejścia przy użyciu ASP.NET WebAPI 2 i działa dobrze).

Kellerman Rivero
źródło
2
Przewodnik po zaleceniach W3 nie ma tu znaczenia, ponieważ jest zgodny ze specyfikacją HTML 4.
Johann
1
Bardzo prawda… „dane inne niż ASCII” wymagają wielu elementów? W XXI wieku? W świecie UTF-8? Jest to oczywiście absurdalna rekomendacja na dziś. Jestem nawet zaskoczony, że istniały w HTML 4 dni, ale czasami świat infrastruktury internetowej porusza się bardzo powoli.
Ray Toal