Wie lade ich eine Datei mit Metadaten über einen REST-Webdienst hoch?

249

Ich habe einen REST-Webdienst, der derzeit diese URL verfügbar macht:

http: // server / data / media

wo Benutzer können POSTdie folgenden JSON:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

um neue Medienmetadaten zu erstellen.

Jetzt muss ich die Möglichkeit haben, eine Datei gleichzeitig mit den Medienmetadaten hochzuladen. Was ist der beste Weg, dies zu tun? Ich könnte eine neue Eigenschaft namens " filebase64" einführen und die Datei codieren, aber ich habe mich gefragt, ob es einen besseren Weg gibt.

Es wird auch verwendet, multipart/form-datawie ein HTML-Formular senden würde, aber ich verwende einen REST-Webdienst und möchte, wenn möglich, bei der Verwendung von JSON bleiben.

Daniel T.
quelle
35
Das Festhalten an nur JSON ist nicht unbedingt erforderlich, um einen RESTful-Webdienst zu haben. REST ist im Grunde alles, was den Hauptprinzipien der HTTP-Methoden und einigen anderen (möglicherweise nicht standardisierten) Regeln folgt.
Erik Kaplun

Antworten:

192

Ich stimme Greg zu, dass ein Zwei-Phasen-Ansatz eine vernünftige Lösung ist, aber ich würde es umgekehrt machen. Ich würde tun:

POST http://server/data/media
body:
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

So erstellen Sie den Metadateneintrag und geben eine Antwort zurück wie:

201 Created
Location: http://server/data/media/21323
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentUrl": "http://server/data/media/21323/content"
}

Der Client kann dann diese ContentUrl verwenden und einen PUT mit den Dateidaten durchführen.

Das Schöne an diesem Ansatz ist, dass die URL, die Sie zurückgeben, nur auf einen anderen Server mit mehr Speicherplatz / Kapazität verweisen kann, wenn Ihr Server mit immensen Datenmengen belastet wird. Oder Sie könnten einen Round-Robin-Ansatz implementieren, wenn die Bandbreite ein Problem darstellt.

Darrel Miller
quelle
8
Ein Vorteil beim erstmaligen Senden des Inhalts besteht darin, dass der Inhalt zum Zeitpunkt des Vorhandenseins der Metadaten bereits vorhanden ist. Letztendlich hängt die richtige Antwort von der Organisation der Daten im System ab.
Greg Hewgill
Danke, ich habe dies als die richtige Antwort markiert, weil ich dies tun wollte. Leider müssen wir aufgrund einer seltsamen Geschäftsregel zulassen, dass der Upload in beliebiger Reihenfolge erfolgt (Metadaten zuerst oder Datei zuerst). Ich habe mich gefragt, ob es eine Möglichkeit gibt, beide zu kombinieren, um die Kopfschmerzen beim Umgang mit beiden Situationen zu vermeiden.
Daniel T.
@Daniel Wenn Sie die Datendatei zuerst POSTEN, können Sie die in Location zurückgegebene URL zum ContentUrl-Attribut in den Metadaten hinzufügen. Wenn der Server die Metadaten empfängt und eine ContentUrl vorhanden ist, weiß er auf diese Weise bereits, wo sich die Datei befindet. Wenn es keine ContentUrl gibt, weiß es, dass es eine erstellen sollte.
Darrel Miller
Wenn Sie zuerst den POST durchführen würden, würden Sie unter derselben URL posten? (/ server / data / media) oder würden Sie einen weiteren Einstiegspunkt für das Hochladen von Dateien erstellen?
Matt Brailsford
1
@Faraway Was ist, wenn die Metadaten die Anzahl der "Likes" eines Bildes enthalten? Würden Sie es dann als eine einzige Ressource behandeln? Oder schlagen Sie vor, dass ich das Bild erneut hochladen müsste, wenn ich die Beschreibung eines Bildes bearbeiten wollte? Es gibt viele Fälle, in denen mehrteilige Formulare die richtige Lösung sind. Das ist einfach nicht immer der Fall.
Darrel Miller
103

Nur weil Sie nicht den gesamten Anforderungshauptteil in JSON einschließen, bedeutet dies nicht, dass es nicht RESTful ist, multipart/form-datasowohl JSON als auch die Datei (en) in einer einzigen Anforderung zu veröffentlichen:

curl -F "metadata=<metadata.json" -F "[email protected]" http://example.com/add-file

auf der Serverseite (mit Python als Pseudocode):

class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

Um mehrere Dateien hochzuladen, können Sie entweder separate "Formularfelder" für jedes verwenden:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... in diesem Fall hat der Servercode request.args['file1'][0]undrequest.args['file2'][0]

oder verwenden Sie das gleiche für viele:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... in diesem Fall request.args['files']wird einfach eine Liste der Länge 2 sein.

oder übergeben Sie mehrere Dateien durch ein einzelnes Feld:

curl -F "metadata=<metadata.json" -F "[email protected],some-other-file.tar.gz" http://example.com/add-file

... in diesem Fall request.args['files']wird es eine Zeichenfolge sein, die alle Dateien enthält, die Sie selbst analysieren müssen - nicht sicher, wie es geht, aber ich bin sicher, dass es nicht schwierig ist, oder besser nur die vorherigen Ansätze verwenden.

Der Unterschied zwischen @und <besteht darin, @dass die Datei als Datei-Upload angehängt wird, während <der Inhalt der Datei als Textfeld angehängt wird .

PS Nur weil ich curldie POSTAnforderungen generiere , heißt das nicht, dass nicht genau dieselben HTTP-Anforderungen von einer Programmiersprache wie Python oder einem ausreichend leistungsfähigen Tool gesendet werden konnten.

Erik Kaplun
quelle
4
Ich hatte mich selbst über diesen Ansatz gewundert und warum ich noch niemanden gesehen hatte, der ihn vorbrachte. Ich stimme zu, scheint mir vollkommen RESTful.
Suppenhund
1
JA! Dies ist ein sehr praktischer Ansatz, und er ist nicht weniger RESTful als die Verwendung von "application / json" als Inhaltstyp für die gesamte Anforderung.
sickill
..aber das ist nur möglich, wenn Sie die Daten in einer .json-Datei haben und sie hochladen, was nicht der Fall ist
itsjavi
5
@mjolnic Ihr Kommentar ist irrelevant: Die cURL-Beispiele sind nur Beispiele . In der Antwort heißt es ausdrücklich, dass Sie alles verwenden können, um die Anfrage abzusenden. Was hindert Sie daran, nur zu schreiben curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun
3
Ich verwende diesen Ansatz, weil die akzeptierte Antwort für die von mir entwickelte Anwendung nicht funktioniert (die Datei kann nicht vor den Daten vorhanden sein und erhöht die Komplexität, um den Fall zu behandeln, in dem die Daten zuerst hochgeladen werden und die Datei nie hochgeladen wird). .
BitsEvolved
33

Eine Möglichkeit, sich dem Problem zu nähern, besteht darin, den Upload in zwei Phasen durchzuführen. Zunächst laden Sie die Datei selbst mithilfe eines POST hoch, wobei der Server eine Kennung an den Client zurückgibt (eine Kennung kann die SHA1 des Dateiinhalts sein). Dann ordnet eine zweite Anforderung die Metadaten den Dateidaten zu:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}

Durch die Einbeziehung der in die JSON-Anforderung selbst codierten Datei data64 64 wird die Größe der übertragenen Daten um 33% erhöht. Dies kann abhängig von der Gesamtgröße der Datei wichtig sein oder auch nicht.

Ein anderer Ansatz könnte darin bestehen, einen POST der Rohdateidaten zu verwenden, jedoch alle Metadaten in den HTTP-Anforderungsheader aufzunehmen. Dies liegt jedoch etwas außerhalb der grundlegenden REST-Operationen und kann für einige HTTP-Client-Bibliotheken umständlicher sein.

Greg Hewgill
quelle
Sie können Ascii85 verwenden, indem Sie es nur um 1/4 erhöhen.
Singagirl
Gibt es einen Hinweis darauf, warum base64 die Größe so stark erhöht?
Jam01
1
@ jam01: Zufälligerweise habe ich gestern gerade etwas gesehen, das die Speicherplatzfrage gut beantwortet: Wie
Greg Hewgill
10

Mir ist klar, dass dies eine sehr alte Frage ist, aber hoffentlich hilft dies jemand anderem, als ich auf diesen Beitrag stieß und nach dem gleichen suchte. Ich hatte ein ähnliches Problem, nur dass meine Metadaten Guid und Int waren. Die Lösung ist jedoch dieselbe. Sie können die benötigten Metadaten einfach in die URL aufnehmen.

POST-Akzeptanzmethode in Ihrer "Controller" -Klasse:

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)
{
    //See http://stackoverflow.com/a/10327789/431906 for how to accept a file
    return null;
}

Dann, in was auch immer Sie Routen registrieren, WebApiConfig.Register (HttpConfiguration config) für mich in diesem Fall.

config.Routes.MapHttpRoute(
    name: "FooController",
    routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}",
    defaults: new { }
);
Greg Biles
quelle
5

Wenn Ihre Datei und ihre Metadaten eine Ressource erstellen, ist es vollkommen in Ordnung, beide in einer Anfrage hochzuladen. Beispielanforderung wäre:

POST https://target.com/myresources/resourcename HTTP/1.1

Accept: application/json

Content-Type: multipart/form-data; 

boundary=-----------------------------28947758029299

Host: target.com

-------------------------------28947758029299

Content-Disposition: form-data; name="application/json"

{"markers": [
        {
            "point":new GLatLng(40.266044,-74.718479), 
            "homeTeam":"Lawrence Library",
            "awayTeam":"LUGip",
            "markerImage":"images/red.png",
            "information": "Linux users group meets second Wednesday of each month.",
            "fixture":"Wednesday 7pm",
            "capacity":"",
            "previousScore":""
        },
        {
            "point":new GLatLng(40.211600,-74.695702),
            "homeTeam":"Hamilton Library",
            "awayTeam":"LUGip HW SIG",
            "markerImage":"images/white.png",
            "information": "Linux users can meet the first Tuesday of the month to work out harward and configuration issues.",
            "fixture":"Tuesday 7pm",
            "capacity":"",
            "tv":""
        },
        {
            "point":new GLatLng(40.294535,-74.682012),
            "homeTeam":"Applebees",
            "awayTeam":"After LUPip Mtg Spot",
            "markerImage":"images/newcastle.png",
            "information": "Some of us go there after the main LUGip meeting, drink brews, and talk.",
            "fixture":"Wednesday whenever",
            "capacity":"2 to 4 pints",
            "tv":""
        },
] }

-------------------------------28947758029299

Content-Disposition: form-data; name="name"; filename="myfilename.pdf"

Content-Type: application/octet-stream

%PDF-1.4
%
2 0 obj
<</Length 57/Filter/FlateDecode>>stream
x+r
26S00SI2P0Qn
F
!i\
)%!Y0i@.k
[
endstream
endobj
4 0 obj
<</Type/Page/MediaBox[0 0 595 842]/Resources<</Font<</F1 1 0 R>>>>/Contents 2 0 R/Parent 3 0 R>>
endobj
1 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>
endobj
3 0 obj
<</Type/Pages/Count 1/Kids[4 0 R]>>
endobj
5 0 obj
<</Type/Catalog/Pages 3 0 R>>
endobj
6 0 obj
<</Producer(iTextSharp 5.5.11 2000-2017 iText Group NV \(AGPL-version\))/CreationDate(D:20170630120636+02'00')/ModDate(D:20170630120636+02'00')>>
endobj
xref
0 7
0000000000 65535 f 
0000000250 00000 n 
0000000015 00000 n 
0000000338 00000 n 
0000000138 00000 n 
0000000389 00000 n 
0000000434 00000 n 
trailer
<</Size 7/Root 5 0 R/Info 6 0 R/ID [<c7c34272c2e618698de73f4e1a65a1b5><c7c34272c2e618698de73f4e1a65a1b5>]>>
%iText-5.5.11
startxref
597
%%EOF

-------------------------------28947758029299--
Mike Ezzati
quelle
3

Ich verstehe nicht, warum im Laufe von acht Jahren niemand die einfache Antwort veröffentlicht hat. Codieren Sie den json nicht als base64, sondern als Zeichenfolge. Dann dekodieren Sie einfach den JSON auf der Serverseite.

In Javascript:

let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

POST es mit Content-Type: Multipart / Formulardaten

Rufen Sie auf der Serverseite die Datei normal ab und rufen Sie den JSON als Zeichenfolge ab. Konvertieren Sie die Zeichenfolge in ein Objekt, bei dem es sich normalerweise um eine Codezeile handelt, unabhängig davon, welche Programmiersprache Sie verwenden.

(Ja, es funktioniert großartig. In einer meiner Apps.)

ccleve
quelle