API REST - Traitement de fichiers (ie images) - Meilleures pratiques

198

Nous développons un serveur avec l'API REST, qui accepte et répond avec JSON. Le problème est, si vous devez télécharger des images du client au serveur.

Remarque: et je parle aussi d'un cas d'utilisation où l'entité (utilisateur) peut avoir plusieurs fichiers (carPhoto, licensePhoto) et également avoir d'autres propriétés (nom, email ...), mais lorsque vous créez un nouvel utilisateur, vous ne N'envoyez pas ces images, elles sont ajoutées après le processus d'inscription.


Les solutions dont j'ai connaissance, mais chacune d'elles présente des défauts

1. Utilisez multipart / form-data au lieu de JSON

bon : les requêtes POST et PUT sont aussi RESTful que possible, elles peuvent contenir des entrées de texte avec un fichier.

inconvénients : Ce n'est plus JSON, ce qui est beaucoup plus facile à tester, déboguer, etc. par rapport à multipart / form-data

2. Autoriser la mise à jour de fichiers séparés

La requête POST pour créer un nouvel utilisateur ne permet pas d'ajouter des images (ce qui est correct dans notre cas d'utilisation comme je l'ai dit au début), le téléchargement des images se fait par requête PUT en multipart / form-data vers par exemple / users / 4 / carPhoto

bon : tout (sauf le fichier qui se télécharge) reste en JSON, il est facile de tester et de déboguer (vous pouvez enregistrer des requêtes JSON complètes sans avoir peur de leur longueur)

inconvénients : Ce n'est pas intuitif, vous ne pouvez pas POST ou PUT toutes les variables de l'entité à la fois et cette adresse /users/4/carPhotopeut également être considérée comme une collection (le cas d'utilisation standard de l'API REST ressemble à ceci /users/4/shipments). Habituellement, vous ne pouvez pas (et ne voulez pas) GET / PUT chaque variable d'entité, par exemple users / 4 / name. Vous pouvez obtenir le nom avec GET et le modifier avec PUT à users / 4. S'il y a quelque chose après l'identifiant, il s'agit généralement d'une autre collection, comme users / 4 / reviews

3. Utilisez Base64

Envoyez-le au format JSON mais encodez les fichiers avec Base64.

bon : Identique à la première solution, c'est le service le plus REST possible.

inconvénients : Une fois de plus, les tests et le débogage sont bien pires (le corps peut avoir des mégaoctets de données), il y a augmentation de la taille et aussi du temps de traitement à la fois - client et serveur


Je voudrais vraiment utiliser la solution no. 2, mais il a ses inconvénients ... N'importe qui peut me donner un meilleur aperçu de la solution "quelle est la meilleure"?

Mon objectif est d'avoir des services RESTful avec autant de standards inclus que possible, tout en souhaitant que les choses restent aussi simples que possible.

libik
la source
Vous pourriez également trouver cela utile: stackoverflow.com/questions/4083702/…
Markon
5
Je sais que ce sujet est ancien, mais nous avons été confrontés à ce problème récemment. La meilleure approche que nous ayons est similaire à la vôtre numéro 2. Nous téléchargeons des fichiers directement vers l'API, puis nous attachons ces fichiers dans le modèle. Avec ce scénario, vous pouvez créer des images de téléchargement avant, après ou sur la même page que le formulaire, cela n'a pas vraiment d'importance. Bonne discussion!
Tiago Matos
2
@TiagoMatos - oui, exactement, je l'ai décrit dans une réponse que j'ai récemment acceptée
libik
6
Merci d'avoir posé cette question.
Zuhayer Tahir
1
"aussi cette adresse / users / 4 / carPhoto peut être davantage considérée comme une collection" - non, elle ne ressemble pas à une collection et ne serait pas nécessairement considérée comme une collection. C'est très bien d'avoir une relation avec une ressource qui n'est pas une collection mais une ressource unique.
B12Toaster

Réponses:

156

OP ici (je réponds à cette question au bout de deux ans, le post de Daniel Cerecedo n'était pas mal à la fois, mais les services web se développent très vite)

Après trois ans de développement logiciel à plein temps (en mettant également l'accent sur l'architecture logicielle, la gestion de projet et l'architecture de microservices), je choisis définitivement la deuxième méthode (mais avec un point final général) comme la meilleure.

Si vous avez un point final spécial pour les images, cela vous donne beaucoup plus de pouvoir sur la gestion de ces images.

Nous avons la même API REST (Node.js) pour les applications mobiles (iOS / Android) et frontend (utilisant React). Nous sommes en 2017, vous ne voulez donc pas stocker les images localement, vous voulez les télécharger sur un stockage cloud (Google cloud, s3, cloudinary, ...), donc vous voulez une manipulation générale sur elles.

Notre flux typique est que dès que vous sélectionnez une image, elle commence à télécharger en arrière-plan (généralement POST sur / images endpoint), vous renvoyant l'ID après le téléchargement. C'est vraiment convivial, car l'utilisateur choisit une image et passe généralement avec d'autres champs (c'est-à-dire adresse, nom, ...), donc quand il clique sur le bouton «envoyer», l'image est généralement déjà téléchargée. Il n'attend pas et regarde l'écran disant "uploading ...".

Il en va de même pour obtenir des images. Surtout grâce aux téléphones mobiles et aux données mobiles limitées, vous ne voulez pas envoyer d'images originales, vous voulez envoyer des images redimensionnées, afin qu'elles ne prennent pas autant de bande passante (et pour rendre vos applications mobiles plus rapides, vous ne voulez souvent pas pour le redimensionner, vous voulez que l'image s'intègre parfaitement dans votre vue). Pour cette raison, les bonnes applications utilisent quelque chose comme cloudinary (ou nous avons notre propre serveur d'images pour le redimensionnement).

De plus, si les données ne sont pas privées, vous renvoyez à l'application / frontend juste une URL et il les télécharge directement à partir du stockage en nuage, ce qui représente une énorme économie de bande passante et de temps de traitement pour votre serveur. Dans nos plus grandes applications, il y a beaucoup de téraoctets téléchargés chaque mois, vous ne voulez pas gérer cela directement sur chacun de votre serveur d'API REST, qui est axé sur le fonctionnement CRUD. Vous souhaitez gérer cela à un seul endroit (notre serveur d'images, qui dispose de la mise en cache, etc.) ou laisser les services cloud gérer tout cela.


Inconvénients: Le seul «inconvénient» auquel vous devriez penser est «images non attribuées». L'utilisateur sélectionne des images et continue à remplir d'autres champs, mais ensuite il dit "non" et éteint l'application ou l'onglet, mais en attendant, vous avez téléchargé l'image avec succès. Cela signifie que vous avez téléchargé une image qui n'est attribuée nulle part.

Il existe plusieurs façons de gérer cela. Le plus simple est "Je m'en fiche", qui est pertinent, si cela ne se produit pas très souvent ou si vous avez même le désir de stocker toutes les images que les utilisateurs vous envoient (pour une raison quelconque) et que vous n'en voulez pas effacement.

Un autre est facile aussi - vous avez CRON et c'est-à-dire chaque semaine et vous supprimez toutes les images non attribuées de plus d'une semaine.

libik
la source
Que se passera-t-il si [dès que vous sélectionnez l'image, le téléchargement commence en arrière-plan (généralement POST sur / images endpoint), vous renvoyant l'ID après le téléchargement] lorsque la demande a échoué en raison d'une connexion Internet? Allez-vous inviter l'utilisateur pendant qu'il poursuit avec d'autres champs (par exemple, adresse, nom, ...)? Je parie que vous allez toujours attendre que l'utilisateur clique sur le bouton "envoyer" et réessayer votre demande, faites-les attendre en regardant l'écran disant "uploadiing ...".
Adromil Balais
5
@AdromilBalais - L'API RESTful est sans état, donc elle ne fait rien (le serveur ne suit pas l'état du consommateur). Le consommateur du service (c'est-à-dire la page Web ou l'appareil mobile) est responsable du traitement des demandes ayant échoué, par conséquent, le consommateur doit décider s'il appelle immédiatement la même demande après l'échec de celle-ci ou ce qu'il faut faire (c'est-à-dire afficher le message "Le téléchargement de l'image a échoué - vous voulez réessayer ")
libik
2
Réponse très informative et éclairante. Merci de répondre.
Zuhayer Tahir
Cela ne résout pas vraiment le problème initial. Cela dit simplement "utiliser un service cloud"
Martin Muzatko
3
@MartinMuzatko - c'est le cas, il choisit la deuxième option et vous indique comment l'utiliser et pourquoi. Si vous voulez dire "mais ce n'est pas une option parfaite qui vous permet de tout envoyer en une seule demande et sans implication" - oui, il n'y a malheureusement pas de telle solution.
libik
105

Il y a plusieurs décisions à prendre :

  1. Le premier sur le chemin des ressources :

    • Modélisez l'image en tant que ressource à elle seule:

      • Nested in user (/ user /: id / image): la relation entre l'utilisateur et l'image se fait implicitement

      • Dans le chemin racine (/ image):

        • Le client est tenu responsable de l'établissement de la relation entre l'image et l'utilisateur, ou;

        • Si un contexte de sécurité est fourni avec la requête POST utilisée pour créer une image, le serveur peut implicitement établir une relation entre l'utilisateur authentifié et l'image.

    • Incorporer l'image dans le cadre de l'utilisateur

  2. La deuxième décision concerne la manière de représenter la ressource image :

    • En tant que charge utile JSON encodée en base 64
    • En tant que charge utile en plusieurs parties

Ce serait ma piste de décision:

  • Je préfère généralement le design à la performance, à moins qu'il n'y ait de solides arguments en faveur de cela. Cela rend le système plus maintenable et peut être plus facilement compris par les intégrateurs.
  • Ma première pensée est donc d'opter pour une représentation Base64 de la ressource image car elle vous permet de tout conserver en JSON. Si vous avez choisi cette option, vous pouvez modéliser le chemin de la ressource comme vous le souhaitez.
    • Si la relation entre l'utilisateur et l'image est de 1 à 1, je préférerais modéliser l'image en tant qu'attribut, spécialement si les deux ensembles de données sont mis à jour en même temps. Dans tous les autres cas, vous pouvez choisir librement de modéliser l'image soit en tant qu'attribut, en la mettant à jour via PUT ou PATCH, ou en tant que ressource distincte.
  • Si vous choisissez une charge utile en plusieurs parties, je me sentirais obligé de modéliser l'image en tant que ressource propre, de sorte que les autres ressources, dans notre cas, la ressource utilisateur, ne soient pas affectées par la décision d'utiliser une représentation binaire pour l'image.

Vient ensuite la question: y a-t-il un impact sur les performances à propos du choix de base64 vs multipart? . On pourrait penser que l'échange de données au format multipart devrait être plus efficace. Mais cet article montre à quel point les deux représentations diffèrent peu en termes de taille.

Mon choix Base64:

  • Décision de conception cohérente
  • Impact négligeable sur les performances
  • Comme les navigateurs comprennent les URI de données (images encodées en base64), il n'est pas nécessaire de les transformer si le client est un navigateur
  • Je ne voterai pas sur l'opportunité de l'avoir en tant qu'attribut ou ressource autonome, cela dépend de votre domaine de problème (que je ne sais pas) et de vos préférences personnelles.
Daniel Cerecedo
la source
3
Ne pouvons-nous pas encoder les données en utilisant d'autres protocoles de sérialisation comme protobuf, etc.? Fondamentalement, j'essaie de comprendre s'il existe d'autres moyens plus simples de traiter l'augmentation de la taille et du temps de traitement qui vient avec l'encodage base64.
Andy Dufresne
1
Réponse très engageante. merci pour l'approche étape par étape. Cela m'a permis de mieux comprendre vos points.
Zuhayer Tahir
13

Votre deuxième solution est probablement la plus correcte. Vous devez utiliser les spécifications HTTP et les types MIME comme prévu et télécharger le fichier via multipart/form-data. En ce qui concerne la gestion des relations, j'utiliserais ce processus (en gardant à l'esprit que je n'en sais rien sur vos hypothèses ou la conception du système):

  1. POSTpour /userscréer l'entité utilisateur.
  2. POSTl'image vers /images, en veillant à renvoyer un en- Locationtête où l'image peut être récupérée selon les spécifications HTTP.
  3. PATCHà /users/carPhotoet attribuez-lui l'identifiant de la photo donnée dans l'en- Locationtête de l'étape 2.
mmcclannahan
la source
1
Je n'ai aucun contrôle direct sur "comment le client utilisera l'API" ... Le problème est que les images "mortes" qui ne sont pas corrigées sur certaines ressources ...
libik
4
Habituellement, lorsque vous choisissez la deuxième option, il est préférable de télécharger d'abord l'élément multimédia et de renvoyer l'identifiant multimédia au client, puis le client peut envoyer les données d'entité, y compris l'identifiant multimédia, cette approche évite les entités cassées ou les informations d'incohérence.
Kellerman Rivero
2

Il n'y a pas de solution simple. Chaque façon a ses avantages et ses inconvénients. Mais la manière canonique est en utilisant la première option: multipart/form-data. Comme le dit le guide de recommandation W3

Le type de contenu «multipart / form-data» doit être utilisé pour soumettre des formulaires contenant des fichiers, des données non ASCII et des données binaires.

Nous n'envoyons pas de formulaires, vraiment, mais le principe implicite s'applique toujours. L'utilisation de base64 comme représentation binaire est incorrecte car vous utilisez le mauvais outil pour atteindre votre objectif, en revanche, la deuxième option oblige vos clients API à faire plus de travail afin de consommer votre service API. Vous devez faire le travail acharné côté serveur afin de fournir une API facile à utiliser. La première option n'est pas facile à déboguer, mais quand vous le faites, elle ne change probablement jamais.

En utilisant, multipart/form-datavous êtes fidèle à la philosophie REST / http. Vous pouvez voir une réponse à une question similaire ici .

Une autre option si vous mélangez les alternatives, vous pouvez utiliser multipart / form-data mais au lieu d'envoyer chaque valeur séparément, vous pouvez envoyer une valeur nommée payload avec la charge utile json à l'intérieur. (J'ai essayé cette approche en utilisant ASP.NET WebAPI 2 et fonctionne bien).

Kellerman Rivero
la source
2
Ce guide de recommandation W3 n'est pas pertinent ici, car il est dans le contexte de la spécification HTML 4.
Johann
1
Très vrai .... "données non ASCII" nécessite plusieurs parties? Au XXIe siècle? Dans un monde UTF-8? Bien sûr, c'est une recommandation ridicule pour aujourd'hui. Je suis même surpris que cela existait dans les jours de HTML 4, mais parfois le monde de l'infrastructure Internet évolue très lentement.
Ray Toal