web-dev-qa-db-fra.com

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

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

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

où les utilisateurs peuvent POST le JSON suivant:

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

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

Maintenant, j'ai besoin de la possibilité de télécharger un fichier en même temps que les métadonnées du média. Quelle est la meilleure façon de s'y prendre? Je pourrais introduire une nouvelle propriété appelée file et encoder le fichier en base64, mais je me demandais s’il existait un meilleur moyen.

Il existe également une utilisation de multipart/form-data comme ce qu’un formulaire HTML enverrait, mais j’utilise un service Web REST et je souhaite, dans la mesure du possible, utiliser JSON.

229
Daniel T.

Je conviens 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 du type:

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 ce ContentUrl et effectuer un PUT avec les données du fichier.

La bonne chose à propos de cette approche est que lorsque votre serveur commence à être surchargé d’immenses volumes de données, l’URL que vous renvoyez ne peut que pointer vers un autre serveur disposant de plus d’espace/capacité. Vous pouvez également mettre en œuvre une approche à la ronde si la bande passante pose problème.

177
Darrel Miller

Ce n’est pas parce que vous n’enveloppez pas le corps entier de la demande dans JSON que cela n’est pas RESTful d’utiliser multipart/form-data pour publier à la fois le JSON et le (s) fichier (s) 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 des "champs de formulaire" distincts pour chacun:

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

... dans quel cas le code serveur aura request.args['file1'][0] et request.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 passe plusieurs fichiers dans un seul champ en une fois:

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 - vous ne savez pas comment le faire, mais je suis sûr que ce n'est pas difficile, ou mieux, utilisez simplement le approches précédentes.

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

PS Ce n'est pas parce que j'utilise curl de générer les demandes POST que le même HTTP est utilisé les requêtes ne pouvaient pas être envoyées à partir d'un langage de programmation tel que Python ou à l'aide d'un outil suffisamment performant.

98
Erik Allik

Une façon d'aborder le problème consiste à transformer le téléchargement en un processus en deux phases. Tout d’abord, vous devez télécharger le fichier lui-même à l’aide d’un POST, le serveur renvoyant 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 des données de fichier base64 codées dans la requête JSON augmentera de 33% la taille des données transférées. Cela peut être important ou non, selon 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 la requête HTTP. Cependant, cela tombe un peu en dehors des opérations de base REST et peut être plus gênant pour certaines bibliothèques de clients HTTP.

31
Greg Hewgill

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

Méthode POST accepting 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, quelle 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 { }
);
10
Greg Biles

Si votre fichier et ses métadonnées créent une ressource, il est parfaitement correct de les télécharger tous les deux dans une requête. La demande d'échantillon 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\
)%[email protected]
[
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--
4
Mike Ezzati

Je ne comprends pas pourquoi, au cours des huit dernières années, personne n’a donné la réponse facile. Plutôt que d'encoder le fichier en tant que base64, encodez le JSON en tant que chaîne. Ensuite, il suffit de décoder le JSON du côté serveur.

En Javascript:

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

POST en utilisant Content-Type: multipart/form-data

Sur le serveur, récupérez le fichier normalement et le json sous forme de chaîne. Convertissez la chaîne en objet, qui correspond généralement à une ligne de code, quel que soit le langage de programmation utilisé.

(Oui, ça marche très bien. Je le fais dans l'une de mes applications.)

1
ccleve