REST API - Dateiverarbeitung (dh Bilder) - Best Practices

194

Wir entwickeln einen Server mit REST-API, der JSON akzeptiert und mit JSON antwortet. Das Problem ist, wenn Sie Bilder vom Client auf den Server hochladen müssen.

Hinweis: und ich spreche auch von einem Anwendungsfall, bei dem die Entität (Benutzer) mehrere Dateien (carPhoto, licensePhoto) und auch andere Eigenschaften (Name, E-Mail ...) haben kann, aber wenn Sie einen neuen Benutzer erstellen, tun Sie dies nicht Senden Sie diese Bilder nicht, sie werden nach dem Registrierungsprozess hinzugefügt.


Die Lösungen sind mir bekannt, aber jede hat einige Mängel

1. Verwenden Sie Multipart / Formulardaten anstelle von JSON

gut : POST- und PUT-Anforderungen sind so RESTful wie möglich, sie können Texteingaben zusammen mit einer Datei enthalten.

Nachteile : Es ist nicht mehr JSON, was viel einfacher zu testen, zu debuggen usw. ist, verglichen mit mehrteiligen / Formulardaten

2. Ermöglichen Sie das Aktualisieren separater Dateien

Die POST-Anforderung zum Erstellen eines neuen Benutzers erlaubt nicht das Hinzufügen von Bildern (was in unserem Anwendungsfall, wie ich zu Beginn sagte, in Ordnung ist). Das Hochladen von Bildern erfolgt per PUT-Anforderung als mehrteilige / Formulardaten zu beispielsweise / users / 4 / carPhoto

gut : Alles (außer dem Hochladen der Datei selbst) bleibt in JSON, es ist einfach zu testen und zu debuggen (Sie können vollständige JSON-Anforderungen protokollieren, ohne Angst vor ihrer Länge zu haben).

Nachteile : Es ist nicht intuitiv, Sie können nicht alle Variablen der Entität gleichzeitig POSTEN oder PUTEN, und auch diese Adresse /users/4/carPhotokann eher als Sammlung betrachtet werden (der Standardanwendungsfall für die REST-API sieht so aus /users/4/shipments). Normalerweise können (und wollen) Sie nicht jede Entitätsvariable GET / PUT, zum Beispiel users / 4 / name. Sie können den Namen mit GET abrufen und mit PUT unter users / 4 ändern. Wenn nach der ID etwas steht, handelt es sich normalerweise um eine andere Sammlung, z. B. users / 4 / reviews

3. Verwenden Sie Base64

Senden Sie es als JSON, aber codieren Sie Dateien mit Base64.

gut : Wie bei der ersten Lösung ist es ein möglichst ruhiger Service.

Nachteile : Erneut ist das Testen und Debuggen viel schlimmer (der Körper kann Megabyte an Daten haben), es gibt eine Zunahme der Größe und auch der Verarbeitungszeit sowohl auf dem Client als auch auf dem Server


Ich würde wirklich gerne Lösung Nr. Verwenden. 2, aber es hat seine Nachteile ... Kann mir jemand einen besseren Einblick in die "Was ist die beste" Lösung geben?

Mein Ziel ist es, RESTful-Services mit möglichst vielen Standards zu haben, während ich es so einfach wie möglich halten möchte.

libik
quelle
Möglicherweise finden Sie dies auch nützlich: stackoverflow.com/questions/4083702/…
Markon
5
Ich weiß, dass dieses Thema alt ist, aber wir haben dieses Problem kürzlich konfrontiert. Der beste Ansatz, den wir haben, ähnelt Ihrem Nummer 2. Wir laden Dateien direkt in die API hoch und hängen diese Dateien dann an das Modell an. In diesem Szenario können Sie Upload-Bilder vor, nach oder auf derselben Seite wie das Formular erstellen. Dies spielt keine Rolle. Gute Diskussion!
Tiago Matos
2
@TiagoMatos - ja, genau, ich habe es in einer Antwort beschrieben, die ich kürzlich akzeptiert habe
libik
6
Vielen Dank, dass Sie diese Frage gestellt haben.
Zuhayer Tahir
1
"Auch diese Adresse / users / 4 / carPhoto kann eher als Sammlung betrachtet werden" - nein, sie sieht nicht wie eine Sammlung aus und würde nicht unbedingt als eine angesehen werden. Es ist völlig in Ordnung, eine Beziehung zu einer Ressource zu haben, die keine Sammlung, sondern eine einzelne Ressource ist.
B12Toaster

Antworten:

151

OP hier (Ich beantworte diese Frage nach zwei Jahren, der Beitrag von Daniel Cerecedo war zu einem Zeitpunkt nicht schlecht, aber die Webdienste entwickeln sich sehr schnell)

Nach drei Jahren Vollzeit-Softwareentwicklung (mit Schwerpunkt auch auf Softwarearchitektur, Projektmanagement und Microservice-Architektur) wähle ich definitiv den zweiten Weg (aber mit einem allgemeinen Endpunkt) als den besten.

Wenn Sie einen speziellen Endpunkt für Bilder haben, haben Sie viel mehr Möglichkeiten, diese Bilder zu verarbeiten.

Wir haben die gleiche REST-API (Node.js) für beide - mobile Apps (iOS / Android) und Frontend (mit React). Dies ist 2017, daher möchten Sie Bilder nicht lokal speichern, sondern in einen Cloud-Speicher (Google Cloud, S3, Cloudinary, ...) hochladen. Daher möchten Sie eine allgemeine Behandlung dieser Bilder.

Unser typischer Ablauf ist, dass sobald Sie ein Bild auswählen, es im Hintergrund hochgeladen wird (normalerweise POST on / images endpoint) und Ihnen nach dem Hochladen die ID zurückgibt. Dies ist sehr benutzerfreundlich, da der Benutzer ein Bild auswählt und dann normalerweise mit einigen anderen Feldern fortfährt (z. B. Adresse, Name, ...). Wenn er also auf die Schaltfläche "Senden" klickt, wird das Bild normalerweise bereits hochgeladen. Er wartet nicht und schaut auf den Bildschirm mit der Aufschrift "Upload ...".

Gleiches gilt für das Abrufen von Bildern. Insbesondere dank Mobiltelefonen und begrenzten mobilen Daten möchten Sie keine Originalbilder senden, sondern Bilder mit geänderter Größe senden, damit diese nicht so viel Bandbreite beanspruchen (und um Ihre mobilen Apps schneller zu machen, möchten Sie dies häufig nicht Um die Größe überhaupt zu ändern, möchten Sie das Bild, das perfekt in Ihre Ansicht passt. Aus diesem Grund verwenden gute Apps so etwas wie Cloudinary (oder wir haben einen eigenen Image-Server zum Ändern der Größe).

Wenn die Daten nicht privat sind, senden Sie sie nur an die App / das Frontend zurück und laden sie direkt aus dem Cloud-Speicher herunter. Dies spart Bandbreite und Verarbeitungszeit für Ihren Server erheblich. In unseren größeren Apps werden jeden Monat viele Terabyte heruntergeladen. Sie möchten dies nicht direkt auf jedem Ihrer REST-API-Server erledigen, der sich auf den CRUD-Betrieb konzentriert. Sie möchten dies an einem Ort erledigen (unser Imageserver mit Caching usw.) oder Cloud-Services alles erledigen lassen.


Nachteile: Die einzigen "Nachteile", an die Sie denken sollten, sind "nicht zugewiesene Bilder". Der Benutzer wählt Bilder aus und füllt andere Felder aus. Dann sagt er "nah" und deaktiviert die App oder den Tab. In der Zwischenzeit haben Sie das Bild jedoch erfolgreich hochgeladen. Dies bedeutet, dass Sie ein Bild hochgeladen haben, das nirgendwo zugewiesen ist.

Es gibt verschiedene Möglichkeiten, damit umzugehen. Am einfachsten ist "Es ist mir egal", was relevant ist, wenn dies nicht sehr oft vorkommt oder Sie sogar den Wunsch haben, jedes Bild zu speichern, das Ihnen der Benutzer sendet (aus irgendeinem Grund), und Sie möchten keines Streichung.

Ein anderes ist auch einfach - Sie haben CRON und dh jede Woche und Sie löschen alle nicht zugewiesenen Bilder, die älter als eine Woche sind.

libik
quelle
Was passiert, wenn [sobald Sie ein Bild auswählen, das Hochladen im Hintergrund beginnt (normalerweise POST auf / Bilder-Endpunkt) und Ihnen nach dem Hochladen die ID zurückgibt], wenn die Anforderung aufgrund einer Internetverbindung fehlgeschlagen ist? Werden Sie den Benutzer auffordern, während er mit einigen anderen Feldern fortfährt (z. B. Adresse, Name, ...)? Ich wette, Sie werden immer noch warten, bis der Benutzer auf die Schaltfläche "Senden" klickt und Ihre Anfrage erneut versucht. Lassen Sie ihn warten, während Sie auf dem Bildschirm "Upload ..." sehen.
Adromil Balais
5
@AdromilBalais - Die RESTful-API ist zustandslos und führt daher nichts aus (der Server verfolgt den Status des Verbrauchers nicht). Der Consumer of Service (dh eine Webseite oder ein mobiles Gerät) ist für die Bearbeitung fehlgeschlagener Anforderungen verantwortlich. Daher muss der Verbraucher entscheiden, ob er sofort dieselbe Anforderung aufruft, nachdem diese fehlgeschlagen ist, oder was zu tun ist (dh "Bild-Upload fehlgeschlagen - erneut versuchen" anzeigen) ")
libik
2
Sehr informative und aufschlussreiche Antwort. Danke für die Antwort.
Zuhayer Tahir
Dies löst das anfängliche Problem nicht wirklich. Dies sagt nur "Verwenden Sie einen Cloud-Dienst"
Martin Muzatko
3
@MartinMuzatko - es tut, es wählt die zweite Option und sagt Ihnen, wie Sie es verwenden sollten und warum. Wenn Sie meinen "aber dies ist keine perfekte Option, mit der Sie alles in einer Anfrage und ohne Implikation senden können" - ja, es gibt leider keine solche Lösung.
Libik
103

Es sind mehrere Entscheidungen zu treffen :

  1. Der erste über Ressourcenpfad :

    • Modellieren Sie das Bild als eigenständige Ressource:

      • Im Benutzer verschachtelt (/ user /: id / image): Die Beziehung zwischen dem Benutzer und dem Bild wird implizit hergestellt

      • Im Stammpfad (/ image):

        • Der Kunde ist dafür verantwortlich, die Beziehung zwischen dem Bild und dem Benutzer herzustellen, oder;

        • Wenn ein Sicherheitskontext mit der POST-Anforderung bereitgestellt wird, die zum Erstellen eines Images verwendet wird, kann der Server implizit eine Beziehung zwischen dem authentifizierten Benutzer und dem Image herstellen.

    • Betten Sie das Bild als Teil des Benutzers ein

  2. Die zweite Entscheidung betrifft die Darstellung der Bildressource :

    • Als Base 64 codierte JSON-Nutzdaten
    • Als mehrteilige Nutzlast

Dies wäre meine Entscheidung:

  • Normalerweise bevorzuge ich Design gegenüber Leistung, es sei denn, es gibt ein starkes Argument dafür. Dies macht das System wartbarer und kann von Integratoren leichter verstanden werden.
  • Mein erster Gedanke ist also, eine Base64-Darstellung der Bildressource zu erstellen, da Sie damit alles JSON behalten können. Wenn Sie diese Option ausgewählt haben, können Sie den Ressourcenpfad nach Ihren Wünschen modellieren.
    • Wenn die Beziehung zwischen Benutzer und Bild 1 zu 1 ist, würde ich es vorziehen, das Bild als Attribut zu modellieren, insbesondere wenn beide Datensätze gleichzeitig aktualisiert werden. In jedem anderen Fall können Sie das Bild frei modellieren, entweder als Attribut, Aktualisierung über PUT oder PATCH oder als separate Ressource.
  • Wenn Sie sich für eine mehrteilige Nutzlast entscheiden, wäre ich gezwungen, das Bild als eigenständige Ressource zu modellieren, damit andere Ressourcen, in unserem Fall die Benutzerressource, nicht von der Entscheidung betroffen sind, eine binäre Darstellung für das Bild zu verwenden.

Dann kommt die Frage: Gibt es irgendwelche Auswirkungen auf die Leistung bei der Auswahl von base64 gegenüber multipart? . Wir könnten denken, dass der Datenaustausch im mehrteiligen Format effizienter sein sollte. Aber dieser Artikel zeigt , wie wenig die beiden Darstellungen unterscheiden sich in Bezug auf Größe.

Meine Wahl Base64:

  • Konsequente Designentscheidung
  • Vernachlässigbare Auswirkungen auf die Leistung
  • Da Browser Daten-URIs (Base64-codierte Bilder) verstehen, müssen diese nicht transformiert werden, wenn der Client ein Browser ist
  • Ich werde nicht darüber abstimmen, ob es als Attribut oder als eigenständige Ressource verwendet werden soll. Dies hängt von Ihrer Problemdomäne (die ich nicht kenne) und Ihrer persönlichen Präferenz ab.
Daniel Cerecedo
quelle
3
Können wir die Daten nicht mit anderen Serialisierungsprotokollen wie Protobuf usw. codieren? Grundsätzlich versuche ich zu verstehen, ob es andere einfachere Möglichkeiten gibt, um die mit der Base64-Codierung verbundene Zunahme von Größe und Verarbeitungszeit zu beheben.
Andy Dufresne
1
Sehr ansprechende Antwort. Vielen Dank für den schrittweisen Ansatz. Ich habe Ihre Punkte viel besser verstanden.
Zuhayer Tahir
13

Ihre zweite Lösung ist wahrscheinlich die richtigste. Sie sollten die HTTP-Spezifikation und die Mimetypen so verwenden, wie sie beabsichtigt waren, und die Datei über hochladen multipart/form-data. Was den Umgang mit den Beziehungen angeht, würde ich diesen Prozess verwenden (wobei ich bedenke, dass ich nichts über Ihre Annahmen oder Ihr Systemdesign weiß):

  1. POSTzu , /usersum die Benutzereinheit zu erstellen.
  2. POSTdas Bild /images, um sicherzustellen , eine Rückkehr - LocationHeader , wo das Bild kann per HTTP - Spezifikation abgerufen werden.
  3. PATCHzu /users/carPhotound weisen Sie die ID des Fotos in den angegebenen Location2 - Header des Schrittes.
mmcclannahan
quelle
1
Ich habe keine direkte Kontrolle darüber, "wie der Client die API verwendet" ... Das Problem dabei ist, dass "tote" Bilder nicht auf einige Ressourcen gepatcht sind ...
libik
4
Wenn Sie die zweite Option auswählen, wird normalerweise bevorzugt, zuerst das Medienelement hochzuladen und die Medienkennung an den Client zurückzugeben. Dann kann der Client die Entitätsdaten einschließlich der Medienkennung senden. Dieser Ansatz vermeidet fehlerhafte Entitäten oder nicht übereinstimmende Informationen.
Kellerman Rivero
2

Es gibt keine einfache Lösung. Jeder Weg hat seine Vor- und Nachteile. Der kanonische Weg ist jedoch die Verwendung der ersten Option : multipart/form-data. Wie der W3-Empfehlungsleitfaden sagt

Der Inhaltstyp "Multipart / Formulardaten" sollte zum Senden von Formularen verwendet werden, die Dateien, Nicht-ASCII-Daten und Binärdaten enthalten.

Wir senden eigentlich keine Formulare, aber das implizite Prinzip gilt immer noch. Die Verwendung von base64 als binäre Darstellung ist falsch, da Sie das falsche Tool verwenden, um Ihr Ziel zu erreichen. Andererseits zwingt die zweite Option Ihre API-Clients, mehr Arbeit zu leisten, um Ihren API-Service zu nutzen. Sie sollten die harte Arbeit auf der Serverseite erledigen, um eine benutzerfreundliche API bereitzustellen. Die erste Option ist nicht einfach zu debuggen, aber wenn Sie dies tun, ändert sie sich wahrscheinlich nie.

Mit multipart/form-dataSie bleiben Sie bei der REST / http-Philosophie. Eine Antwort auf eine ähnliche Frage können Sie hier anzeigen .

Eine andere Option, wenn Sie die Alternativen mischen, können Sie mehrteilige / Formulardaten verwenden. Anstatt jeden Wert separat zu senden, können Sie einen Wert mit dem Namen payload mit der darin enthaltenen json-Payload senden. (Ich habe diesen Ansatz mit ASP.NET WebAPI 2 versucht und funktioniert einwandfrei).

Kellerman Rivero
quelle
2
Dieser W3-Empfehlungsleitfaden ist hier irrelevant, da er im Kontext der HTML 4-Spezifikation steht.
Johann
1
Sehr wahr .... "Nicht-ASCII-Daten" erfordern mehrteilig? Im einundzwanzigsten Jahrhundert? In einer UTF-8-Welt? Das ist natürlich eine lächerliche Empfehlung für heute. Ich bin sogar überrascht, dass es in den HTML 4 Tagen existierte, aber manchmal bewegt sich die Welt der Internetinfrastruktur sehr langsam.
Ray Toal