Publication d'un fichier et des données associées sur un WebService RESTful de préférence au format JSON

757

Ce sera probablement une question stupide, mais je passe une de ces nuits. Dans une application, je développe l'API RESTful et nous voulons que le client envoie des données au format JSON. Une partie de cette application nécessite que le client télécharge un fichier (généralement une image) ainsi que des informations sur l'image.

J'ai du mal à retracer comment cela se produit dans une seule demande. Est-il possible de Base64 les données du fichier dans une chaîne JSON? Vais-je devoir effectuer 2 publications sur le serveur? Ne devrais-je pas utiliser JSON pour cela?

En remarque, nous utilisons Grails sur le backend et ces services sont accessibles par les clients mobiles natifs (iPhone, Android, etc.), si cela fait une différence.

Gregg
la source
1
Alors, quelle est la meilleure façon de procéder?
James111
3
Envoyez les métadonnées dans la chaîne de requête URL, au lieu de JSON.
jrc

Réponses:

632

J'ai posé une question similaire ici:

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

Vous avez essentiellement trois choix:

  1. Base64 encode le fichier, au prix d'une augmentation de la taille des données d'environ 33%, et ajoute une surcharge de traitement sur le serveur et le client pour l'encodage / décodage.
  2. Envoyez d'abord le fichier dans un multipart/form-dataPOST et renvoyez un ID au client. Le client envoie ensuite les métadonnées avec l'ID et le serveur réassocie le fichier et les métadonnées.
  3. Envoyez d'abord les métadonnées et renvoyez un ID au client. Le client envoie ensuite le fichier avec l'ID et le serveur réassocie le fichier et les métadonnées.
Daniel T.
la source
29
Si j'ai choisi l'option 1, dois-je simplement inclure le contenu Base64 dans la chaîne JSON? {fichier: '234JKFDS # $ @ # $ MFDDMS ....', nom: 'somename' ...} Ou y a-t-il autre chose?
Gregg
15
Gregg, exactement comme vous l'avez dit, il vous suffit de l'inclure en tant que propriété, et la valeur serait la chaîne codée en base64. C'est probablement la méthode la plus simple à utiliser, mais peut ne pas être pratique en fonction de la taille du fichier. Par exemple, pour notre application, nous devons envoyer des images iPhone de 2 à 3 Mo chacune. Une augmentation de 33% n'est pas acceptable. Si vous envoyez uniquement de petites images de 20 Ko, cette surcharge pourrait être plus acceptable.
Daniel
19
Je dois également mentionner que l'encodage / décodage base64 prendra également un certain temps de traitement. C'est peut-être la chose la plus simple à faire, mais ce n'est certainement pas la meilleure.
Daniel T.
8
json avec base64? hmm .. je pense à m'en tenir au multipart / formulaire
Omniprésent
12
Pourquoi est-il interdit d'utiliser plusieurs données / formulaires en une seule demande?
1stinct
107

Vous pouvez envoyer le fichier et les données en une seule demande en utilisant le type de contenu multipart / form-data :

Dans de nombreuses applications, il est possible qu'un utilisateur se voit présenter un formulaire. L'utilisateur remplira le formulaire, y compris les informations saisies, générées par la saisie de l'utilisateur ou incluses à partir des fichiers qu'il a sélectionnés. Lorsque le formulaire est rempli, les données du formulaire sont envoyées de l'utilisateur à l'application réceptrice.

La définition de MultiPart / Form-Data est dérivée de l'une de ces applications ...

Sur http://www.faqs.org/rfcs/rfc2388.html :

"multipart / form-data" contient une série de parties. Chaque partie est censée contenir un en-tête de disposition de contenu [RFC 2183] où le type de disposition est "form-data", et où la disposition contient un paramètre (supplémentaire) de "nom", où la valeur de ce paramètre est l'original nom du champ dans le formulaire. Par exemple, une pièce peut contenir un en-tête:

Contenu-Disposition: formulaire-données; nom = "utilisateur"

avec la valeur correspondant à l'entrée du champ "utilisateur".

Vous pouvez inclure des informations de fichier ou des informations de champ dans chaque section entre les limites. J'ai réussi à implémenter un service RESTful qui obligeait l'utilisateur à soumettre à la fois des données et un formulaire, et les données en plusieurs parties / formulaires fonctionnaient parfaitement. Le service a été construit en utilisant Java / Spring, et le client utilisait C #, donc malheureusement je n'ai pas d'exemples Grails à vous donner sur la façon de configurer le service. Vous n'avez pas besoin d'utiliser JSON dans ce cas car chaque section "form-data" vous fournit un endroit pour spécifier le nom du paramètre et sa valeur.

La bonne chose à propos de l'utilisation de multipart / form-data est que vous utilisez des en-têtes définis par HTTP, donc vous vous en tenez à la philosophie REST d'utiliser les outils HTTP existants pour créer votre service.

McStretch
la source
1
Merci, mais ma question était axée sur le fait de vouloir utiliser JSON pour la demande et si c'était possible. Je sais déjà que je pourrais l'envoyer comme vous le suggérez.
Gregg
15
Oui, c'est essentiellement ma réponse pour "Ne devrais-je pas utiliser JSON pour cela?" Y a-t-il une raison spécifique pour laquelle vous souhaitez que le client utilise JSON?
McStretch
3
Très probablement une exigence commerciale ou une cohérence. Bien sûr, la chose idéale à faire est d'accepter les deux (données de formulaire et réponse JSON) sur la base de l'en-tête HTTP Content-Type.
Daniel T.
2
Choisir JSON donne un code beaucoup plus élégant à la fois côté client et côté serveur, ce qui conduit à moins de bugs potentiels. Les données du formulaire le sont hier.
superarts.org
5
Je m'excuse pour ce que j'ai dit si cela blessait le sentiment d'un développeur .Net. Bien que l'anglais ne soit pas ma langue maternelle, ce n'est pas une excuse valable pour moi de dire quelque chose de grossier sur la technologie elle-même. L'utilisation des données de formulaire est géniale et si vous continuez à les utiliser, vous serez encore plus génial!
superarts.org
53

Je sais que ce fil est assez ancien, cependant, il me manque ici une option. Si vous avez des métadonnées (dans n'importe quel format) que vous souhaitez envoyer avec les données à télécharger, vous pouvez faire une seule multipart/relateddemande.

Le type de support Multipart / Related est destiné aux objets composés composés de plusieurs parties du corps liées entre elles.

Vous pouvez vérifier la spécification RFC 2387 pour plus de détails.

Fondamentalement, chaque partie d'une telle demande peut avoir un contenu de type différent et toutes les parties sont en quelque sorte liées (par exemple une image et ses métadonnées). Les pièces sont identifiées par une chaîne de délimitation et la chaîne de délimitation finale est suivie de deux tirets.

Exemple:

POST /upload HTTP/1.1
Host: www.hostname.com
Content-Type: multipart/related; boundary=xyz
Content-Length: [actual-content-length]

--xyz
Content-Type: application/json; charset=UTF-8

{
    "name": "Sample image",
    "desc": "...",
    ...
}

--xyz
Content-Type: image/jpeg

[image data]
[image data]
[image data]
...
--foo_bar_baz--
pgiecek
la source
J'ai préféré de loin votre solution. Malheureusement, il semble qu'il n'y ait aucun moyen de créer des requêtes multi-parties / connexes dans un navigateur.
Petr Baudis
avez-vous une expérience pour amener les clients (en particulier ceux JS) à communiquer avec l'api de cette manière
pvgoddijn
malheureusement, il n'y a actuellement aucun lecteur pour ce type de données sur php (7.2.1) et vous devrez créer votre propre analyseur
dewd
Il est triste que les serveurs et les clients n'aient pas un bon support pour cela.
Nader Ghanbari
14

Je sais que cette question est ancienne, mais au cours des derniers jours, j'avais cherché sur tout le Web pour résoudre cette même question. J'ai des services Web REST Grails et un client iPhone qui envoient des photos, un titre et une description.

Je ne sais pas si mon approche est la meilleure, mais elle est si facile et simple.

Je prends une photo en utilisant le UIImagePickerController et envoie au serveur le NSData en utilisant les balises d'en-tête de demande pour envoyer les données de la photo.

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"myServerAddress"]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:UIImageJPEGRepresentation(picture, 0.5)];
[request setValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"myPhotoTitle" forHTTPHeaderField:@"Photo-Title"];
[request setValue:@"myPhotoDescription" forHTTPHeaderField:@"Photo-Description"];

NSURLResponse *response;

NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

Côté serveur, je reçois la photo en utilisant le code:

InputStream is = request.inputStream

def receivedPhotoFile = (IOUtils.toByteArray(is))

def photo = new Photo()
photo.photoFile = receivedPhotoFile //photoFile is a transient attribute
photo.title = request.getHeader("Photo-Title")
photo.description = request.getHeader("Photo-Description")
photo.imageURL = "temp"    

if (photo.save()) {    

    File saveLocation = grailsAttributes.getApplicationContext().getResource(File.separator + "images").getFile()
    saveLocation.mkdirs()

    File tempFile = File.createTempFile("photo", ".jpg", saveLocation)

    photo.imageURL = saveLocation.getName() + "/" + tempFile.getName()

    tempFile.append(photo.photoFile);

} else {

    println("Error")

}

Je ne sais pas si j'ai des problèmes à l'avenir, mais maintenant, ça fonctionne bien dans l'environnement de production.

Rscorreia
la source
1
J'aime cette option d'utiliser des en-têtes http. Cela fonctionne particulièrement bien lorsqu'il y a une certaine symétrie entre les métadonnées et les en-têtes http standard, mais vous pouvez évidemment inventer les vôtres.
EJ Campbell
14

Voici mon approche API (j'utilise un exemple) - comme vous pouvez le voir, vous je n'utilise aucun file_id(identifiant de fichier téléchargé sur le serveur) dans l'API:

  1. Créer un photoobjet sur le serveur:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Téléchargez le fichier (notez qu'il fileest au singulier car il n'est qu'un par photo):

    POST: /projects/{project_id}/photos/{photo_id}/file
    body: file to upload
    response: -

Et puis par exemple:

  1. Lire la liste des photos

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Lire les détails de la photo

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Lire le fichier photo

    GET: /projects/{project_id}/photos/{photo_id}/file
    response: file content

Donc, la conclusion est que, d'abord, vous créez un objet (photo) par POST, puis vous envoyez une deuxième demande avec le fichier (à nouveau POST).

Kamil Kiełczewski
la source
3
Cela semble être le moyen le plus «RESTFUL» pour y parvenir.
James Webster
Opération POST pour les ressources nouvellement créées, doit renvoyer l'identifiant d'emplacement, dans les détails de la version simple de l'objet
Ivan Proskuryakov
@ivanproskuryakov pourquoi "doit"? Dans l'exemple ci-dessus (POST au point 2), l'ID de fichier est inutile. Deuxième argument (pour POST au point 2) j'utilise la forme singulière '/ fichier' (pas '/ fichiers') donc l'ID n'est pas nécessaire car le chemin: / projets / 2 / photos / 3 / fichier donne des informations COMPLÈTES au fichier photo d'identité.
Kamil Kiełczewski
De la spécification du protocole HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Created "La ressource nouvellement créée peut être référencée par le ou les URI renvoyés dans l'entité de la réponse, avec l'URI le plus spécifique pour la ressource donnée par un champ d'en-tête d'emplacement. " @ KamilKiełczewski (un) et (deux) peuvent être combinés en une seule opération POST POST: / projects / {project_id} / photos Vous renverra votre en-tête d'emplacement, qui pourrait être utilisé pour une opération GET sur une seule photo (ressource *) GET: pour obtenir un une seule photo avec tous les détails CGET: pour obtenir toute la collection des photos
Ivan Proskuryakov
1
Si les métadonnées et le téléchargement sont des opérations distinctes, les points de terminaison ont ces problèmes: Pour l'opération de téléchargement de fichier POST utilisée - POST n'est pas idempotent. PUT (idempotent) doit être utilisé car vous modifiez la ressource sans en créer une nouvelle. REST fonctionne avec des objets appelés ressources . POST: "../photos/" PUT: "../photos/{photo_id}" GET: "../photos/" GET: "../photos/{photo_id}" PS. La séparation du téléchargement dans un point de terminaison distinct peut entraîner un comportement imprévu. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

Objets FormData: télécharger des fichiers à l'aide d'Ajax

XMLHttpRequest Level 2 ajoute la prise en charge de la nouvelle interface FormData. Les objets FormData fournissent un moyen de construire facilement un ensemble de paires clé / valeur représentant des champs de formulaire et leurs valeurs, qui peuvent ensuite être facilement envoyés à l'aide de la méthode XMLHttpRequest send ().

function AjaxFileUpload() {
    var file = document.getElementById("files");
    //var file = fileInput;
    var fd = new FormData();
    fd.append("imageFileData", file);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", '/ws/fileUpload.do');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
             alert('success');
        }
        else if (uploadResult == 'success')
             alert('error');
    };
    xhr.send(fd);
}

https://developer.mozilla.org/en-US/docs/Web/API/FormData

lakhan_Ideavate
la source
6

Étant donné que le seul exemple manquant est l' exemple ANDROID , je vais l'ajouter. Cette technique utilise une AsyncTask personnalisée qui doit être déclarée dans votre classe Activity.

private class UploadFile extends AsyncTask<Void, Integer, String> {
    @Override
    protected void onPreExecute() {
        // set a status bar or show a dialog to the user here
        super.onPreExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // progress[0] is the current status (e.g. 10%)
        // here you can update the user interface with the current status
    }

    @Override
    protected String doInBackground(Void... params) {
        return uploadFile();
    }

    private String uploadFile() {

        String responseString = null;
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://example.com/upload-file");

        try {
            AndroidMultiPartEntity ampEntity = new AndroidMultiPartEntity(
                new ProgressListener() {
                    @Override
                        public void transferred(long num) {
                            // this trigger the progressUpdate event
                            publishProgress((int) ((num / (float) totalSize) * 100));
                        }
            });

            File myFile = new File("/my/image/path/example.jpg");

            ampEntity.addPart("fileFieldName", new FileBody(myFile));

            totalSize = ampEntity.getContentLength();
            httpPost.setEntity(ampEntity);

            // Making server call
            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                responseString = EntityUtils.toString(httpEntity);
            } else {
                responseString = "Error, http status: "
                        + statusCode;
            }

        } catch (Exception e) {
            responseString = e.getMessage();
        }
        return responseString;
    }

    @Override
    protected void onPostExecute(String result) {
        // if you want update the user interface with upload result
        super.onPostExecute(result);
    }

}

Ainsi, lorsque vous souhaitez télécharger votre fichier, appelez simplement:

new UploadFile().execute();
lifeisfoo
la source
Salut, qu'est-ce qu'AndroidMultiPartEntity, veuillez expliquer ... et si je veux télécharger un fichier pdf, word ou xls ce que je dois faire, veuillez donner quelques conseils ... je suis nouveau dans ce domaine.
amit pandya
1
@amitpandya J'ai changé le code en un téléchargement de fichier générique afin qu'il soit plus clair pour quiconque le lit
lifeisfoo
2

Je voulais envoyer des chaînes au serveur principal. Je n'ai pas utilisé json avec plusieurs parties, j'ai utilisé des paramètres de demande.

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void uploadFile(HttpServletRequest request,
        HttpServletResponse response, @RequestParam("uuid") String uuid,
        @RequestParam("type") DocType type,
        @RequestParam("file") MultipartFile uploadfile)

L'URL ressemblerait

http://localhost:8080/file/upload?uuid=46f073d0&type=PASSPORT

Je passe deux paramètres (uuid et type) avec le téléchargement de fichiers. J'espère que cela aidera ceux qui n'ont pas les données json complexes à envoyer.

Anlam anwer
la source
1

Vous pouvez essayer d'utiliser la bibliothèque https://square.github.io/okhttp/ . Vous pouvez définir le corps de la requête sur plusieurs parties, puis ajouter séparément le fichier et les objets json comme suit:

MultipartBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("uploadFile", uploadFile.getName(), okhttp3.RequestBody.create(uploadFile, MediaType.parse("image/png")))
                .addFormDataPart("file metadata", json)
                .build();

        Request request = new Request.Builder()
                .url("https://uploadurl.com/uploadFile")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            logger.info(response.body().string());
OneXer
la source
0
@RequestMapping(value = "/uploadImageJson", method = RequestMethod.POST)
    public @ResponseBody Object jsongStrImage(@RequestParam(value="image") MultipartFile image, @RequestParam String jsonStr) {
-- use  com.fasterxml.jackson.databind.ObjectMapper convert Json String to Object
}
sunleo
la source
-5

Veuillez vous assurer que vous disposez de l'importation suivante. Bien sûr, d'autres importations standard

import org.springframework.core.io.FileSystemResource


    void uploadzipFiles(String token) {

        RestBuilder rest = new RestBuilder(connectTimeout:10000, readTimeout:20000)

        def zipFile = new File("testdata.zip")
        def Id = "001G00000"
        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>()
        form.add("id", id)
        form.add('file',new FileSystemResource(zipFile))
        def urld ='''http://URL''';
        def resp = rest.post(urld) {
            header('X-Auth-Token', clientSecret)
            contentType "multipart/form-data"
            body(form)
        }
        println "resp::"+resp
        println "resp::"+resp.text
        println "resp::"+resp.headers
        println "resp::"+resp.body
        println "resp::"+resp.status
    }
Mak Kul
la source
1
Ce getjava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz