Comment télécharger un fichier avec des métadonnées à l'aide d'un service Web REST?

249

J'ai un service Web REST qui expose actuellement cette URL:

http: // serveur / données / média

où les utilisateurs peuvent POSTle JSON suivant:

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

afin de créer une nouvelle métadonnée Media.

Maintenant, j'ai besoin de pouvoir télécharger un fichier en même temps que les métadonnées multimédias. Quelle est la meilleure façon de procéder? Je pourrais introduire une nouvelle propriété appelée fileet base64 encoder le fichier, mais je me demandais s'il y avait une meilleure façon.

Il y a aussi l'utilisation multipart/form-datacomme ce qu'un formulaire HTML enverrait, mais j'utilise un service Web REST et je veux m'en tenir à JSON si possible.

Daniel T.
la source
35
Il n'est pas vraiment nécessaire de s'en tenir à JSON uniquement pour avoir un service Web RESTful. REST est essentiellement tout ce qui suit les principes principaux des méthodes HTTP et d'autres règles (sans doute non standardisées).
Erik Kaplun

Réponses:

192

Je suis d'accord avec Greg qu'une approche en deux phases est une solution raisonnable, mais je le ferais dans l'autre sens. Je ferais:

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

Pour créer l'entrée de métadonnées et renvoyer une réponse comme:

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

Le client peut ensuite utiliser cette ContentUrl et effectuer un PUT avec les données du fichier.

La bonne chose à propos de cette approche est que lorsque votre serveur commence à s'alourdir avec d'immenses volumes de données, l'url que vous renvoyez peut simplement pointer vers un autre serveur avec plus d'espace / de capacité. Ou vous pouvez implémenter une sorte d'approche à tour de rôle si la bande passante est un problème.

Darrel Miller
la source
8
L'un des avantages de l'envoi du contenu en premier est qu'au moment où les métadonnées existent, le contenu est déjà présent. En fin de compte, la bonne réponse dépend de l'organisation des données dans le système.
Greg Hewgill
Merci, j'ai marqué cela comme la bonne réponse car c'est ce que je voulais faire. Malheureusement, en raison d'une règle commerciale étrange, nous devons autoriser le téléchargement dans n'importe quel ordre (métadonnées en premier ou fichier en premier). Je me demandais s'il y avait un moyen de combiner les deux afin d'éviter le mal de tête de faire face aux deux situations.
Daniel T.
@Daniel Si vous POSTEZ d'abord le fichier de données, vous pouvez prendre l'URL renvoyée dans Location et l'ajouter à l'attribut ContentUrl dans les métadonnées. De cette façon, lorsque le serveur reçoit les métadonnées, si une ContentUrl existe, il sait déjà où se trouve le fichier. S'il n'y a pas ContentUrl, il sait qu'il doit en créer un.
Darrel Miller
si vous deviez faire le POST en premier, publieriez-vous à la même URL? (/ server / data / media) ou créeriez-vous un autre point d'entrée pour les téléchargements en priorité sur les fichiers?
Matt Brailsford
1
@Faraway Et si les métadonnées incluaient le nombre de "j'aime" d'une image? La traiteriez-vous alors comme une ressource unique? Ou plus évidemment, suggérez-vous que si je voulais modifier la description d'une image, je devrais télécharger à nouveau l'image? Il existe de nombreux cas où les formulaires en plusieurs parties sont la bonne solution. Ce n'est pas toujours le cas.
Darrel Miller,
103

Ce n'est pas parce que vous n'encapsulez pas tout le corps de la demande dans JSON que cela n'est pas RESTful à utiliser multipart/form-datapour publier à la fois le JSON et les fichiers dans une seule demande:

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

côté serveur (en utilisant Python pour le pseudocode):

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

pour télécharger plusieurs fichiers, il est possible d'utiliser soit des "champs de formulaire" séparés pour chacun:

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

... auquel cas le code du serveur aura request.args['file1'][0]etrequest.args['file2'][0]

ou réutiliser le même pour plusieurs:

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

... auquel cas request.args['files']sera simplement une liste de longueur 2.

ou passez plusieurs fichiers dans un seul champ:

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

... auquel cas request.args['files']sera une chaîne contenant tous les fichiers, que vous devrez analyser vous-même - je ne sais pas comment le faire, mais je suis sûr que ce n'est pas difficile, ou mieux utilisez simplement les approches précédentes.

La différence entre @ et <est que @le fichier est joint en tant que téléchargement de fichier, tandis que <le contenu du fichier est joint en tant que champ de texte.

PS Ce n'est pas parce que j'utilise curlcomme moyen de générer des POSTrequêtes que les mêmes requêtes HTTP ne peuvent pas être envoyées à partir d'un langage de programmation tel que Python ou en utilisant un outil suffisamment performant.

Erik Kaplun
la source
4
Je m'étais interrogé sur cette approche moi-même, et pourquoi je n'avais encore vu personne d'autre la proposer. Je suis d'accord, me semble parfaitement reposant.
soupdog
1
OUI! C'est une approche très pratique, et elle n'est pas moins RESTful que d'utiliser "application / json" comme type de contenu pour la requête entière.
2014 à 12h04
..mais ce n'est possible que si vous avez les données dans un fichier .json et que vous les téléchargez, ce qui n'est pas le cas
itsjavi
5
@mjolnic votre commentaire n'est pas pertinent: les exemples cURL ne sont que des exemples ; la réponse indique explicitement que vous pouvez utiliser n'importe quoi pour envoyer la demande ... aussi, qu'est-ce qui vous empêche de simplement écrire curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun
3
J'utilise cette approche parce que la réponse acceptée ne fonctionnerait pas pour l'application que je développe (le fichier ne peut pas exister avant les données et cela ajoute une complexité inutile pour gérer le cas où les données sont téléchargées en premier et le fichier ne se télécharge jamais) .
BitsEvolved
33

Une façon d'aborder le problème est de faire du téléchargement un processus en deux phases. Tout d'abord, vous téléchargez le fichier lui-même à l'aide d'un POST, où le serveur renvoie un identifiant au client (un identifiant peut être le SHA1 du contenu du fichier). Ensuite, une deuxième requête associe les métadonnées aux données du fichier:

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

L'inclusion de la base de données de fichiers64 encodée dans la demande JSON elle-même augmentera la taille des données transférées de 33%. Cela peut être important ou non en fonction de la taille globale du fichier.

Une autre approche pourrait consister à utiliser un POST des données de fichier brutes, mais inclure toutes les métadonnées dans l'en-tête de requête HTTP. Cependant, cela se situe un peu en dehors des opérations REST de base et peut être plus gênant pour certaines bibliothèques client HTTP.

Greg Hewgill
la source
Vous pouvez utiliser Ascii85 en augmentant juste de 1/4.
Singagirl
Une référence sur pourquoi base64 augmente autant la taille?
jam01
1
@ jam01: Par coïncidence, je viens de voir quelque chose hier qui répond bien à la question de l'espace: Quelle est la surcharge d'espace du codage Base64?
Greg Hewgill
10

Je me rends compte que c'est une très vieille question, mais j'espère que cela aidera quelqu'un d'autre quand je suis tombé sur ce post à la recherche de la même chose. J'ai eu un problème similaire, juste que mes métadonnées étaient un Guid et un int. La solution est cependant la même. Vous pouvez simplement intégrer les métadonnées nécessaires à l'URL.

Méthode d'acceptation POST dans votre classe "Controller":

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;
}

Ensuite, quel que soit l'enregistrement des itinéraires, WebApiConfig.Register (configuration HttpConfiguration) pour moi dans ce cas.

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

Si votre fichier et ses métadonnées créent une ressource, il est tout à fait correct de les télécharger tous les deux en une seule demande. Un exemple de demande serait:

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
la source
3

Je ne comprends pas pourquoi, en huit ans, personne n'a publié la réponse facile. Plutôt que de coder le fichier en base64, codez le json en tant que chaîne. Il suffit ensuite de décoder le json côté serveur.

En Javascript:

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

POSTEZ-le en utilisant Content-Type: multipart / form-data

Côté serveur, récupérez le fichier normalement et récupérez le json sous forme de chaîne. Convertissez la chaîne en un objet, qui est généralement une ligne de code, quel que soit le langage de programmation que vous utilisez.

(Oui, cela fonctionne très bien. Le faire dans l'une de mes applications.)

ccleve
la source