281 votes

Comment télécharger un fichier avec des métadonnées en utilisant un service web REST ?

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

http://server/data/media

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 Media.

J'ai maintenant 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 procéder ? Je pourrais introduire une nouvelle propriété appelée file et coder le fichier en base64, mais je me demandais s'il y avait un meilleur moyen.

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

45 votes

Il n'est pas vraiment nécessaire de s'en tenir à l'utilisation de JSON pour disposer d'un service web RESTful. REST est en fait tout ce qui suit les grands principes des méthodes HTTP et quelques autres règles (sans doute non standardisées).

214voto

Darrel Miller Points 56797

Je suis d'accord avec Greg pour dire qu'une approche en deux phases est une solution raisonnable, mais je le ferais dans l'autre sens. Je le 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 alors utiliser ce ContentUrl et effectuer un PUT avec les données du fichier.

L'avantage 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 disposant de plus d'espace/capacité. Vous pouvez également mettre en œuvre une sorte d'approche round robin si la bande passante est un problème.

8 votes

L'un des avantages d'envoyer le contenu en premier est qu'au moment où les métadonnées existent, le contenu est déjà présent. En définitive, la bonne réponse dépend de l'organisation des données dans le système.

0 votes

Merci, j'ai marqué cette réponse comme étant la bonne parce que c'est ce que je voulais faire. Malheureusement, en raison d'une règle de gestion étrange, nous devons autoriser le téléchargement dans n'importe quel ordre (métadonnées d'abord ou fichier d'abord). Je me demandais s'il y avait un moyen de combiner les deux afin d'éviter de devoir gérer les deux situations.

0 votes

@Daniel Si vous POST le fichier de données d'abord, vous pouvez prendre l'URL retournée dans Location et l'ajouter à l'attribut ContentUrl dans les métadonnées. Ainsi, lorsque le serveur reçoit les métadonnées, s'il existe un ContentUrl, il sait déjà où se trouve le fichier. S'il n'y a pas de ContentUrl, il sait qu'il doit en créer un.

119voto

Erik Allik Points 9158

Ce n'est pas parce que le corps entier de la requête n'est pas enveloppé dans du JSON que l'utilisation de RESTful n'est pas possible. multipart/form-data pour envoyer à la fois le JSON et le(s) fichier(s) en une seule requête :

curl -F "metadata=<metadata.json" -F "file=@my-file.tar.gz" http://example.com/add-file

du côté du serveur :

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 d'eux :

curl -F "metadata=<metadata.json" -F "file1=@some-file.tar.gz" -F "file2=@some-other-file.tar.gz" http://example.com/add-file

...dans ce cas le code du 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 "files=@some-file.tar.gz" -F "files=@some-other-file.tar.gz" http://example.com/add-file

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

ou faire passer plusieurs fichiers par un seul champ :

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz,some-other-file.tar.gz" http://example.com/add-file

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

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

P.S. Ce n'est pas parce que j'utilise curl comme moyen de générer le POST ne signifie pas que les mêmes requêtes HTTP ne pourraient pas être envoyées à partir d'un langage de programmation tel que Python ou à l'aide de tout autre outil suffisamment performant.

4 votes

Je m'étais moi-même interrogé sur cette approche et sur la raison pour laquelle je n'avais encore vu personne la proposer. Je suis d'accord, cela me semble parfaitement RESTful.

1 votes

OUI ! Cette approche est très pratique, et elle n'est pas moins RESTful que l'utilisation de "application/json" comme type de contenu pour l'ensemble de la requête.

1 votes

Mais cela 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.

37voto

Greg Hewgill Points 356191

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 en utilisant 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 des données du fichier codées en base64 dans la requête 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 brutes du fichier, mais à inclure toutes les métadonnées dans l'en-tête de la requête HTTP. Toutefois, cette approche s'écarte un peu des opérations REST de base et peut s'avérer plus délicate pour certaines bibliothèques client HTTP.

0 votes

Vous pouvez utiliser Ascii85 en augmentant simplement de 1/4.

0 votes

Une référence sur la raison pour laquelle la base64 augmente la taille à ce point ?

1 votes

@jam01 : Par coïncidence, j'ai vu hier quelque chose qui répond bien à la question de l'espace : Quel est l'encombrement de l'encodage Base64 ?

11voto

Greg B Points 206

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 post en cherchant la même chose. J'ai eu un problème similaire, sauf que mes métadonnées étaient un Guid et un int. La solution est cependant la même. Vous pouvez simplement faire en sorte que les métadonnées nécessaires fassent partie de l'URL.

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

Puis dans ce que vous enregistrez comme routes, WebApiConfig.Register(HttpConfiguration config) pour moi dans ce cas.

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

10voto

ccleve Points 3373

Je ne comprends pas pourquoi, en huit ans, personne n'a affiché la réponse facile. Plutôt que d'encoder le fichier en base64, encodez le json sous forme de chaîne. Ensuite, il suffit de décoder le json du côté du 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

Du côté du serveur, récupérez le fichier normalement, et récupérez le json comme une chaîne. Convertissez la chaîne en objet, ce qui représente 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 une de mes applications).

0 votes

Je suis bien plus surpris que personne n'ait développé la réponse de Mike, parce que c'est exactement comme ça que multipartite devrait être utilisé : chaque partie a son propre type de mime et l'analyseur multipart de DRF devrait le distribuer en conséquence. Peut-être est-il difficile de créer ce type d'enveloppe du côté client. Je devrais vraiment me renseigner...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X