web-dev-qa-db-fra.com

Enregistrement d'un fichier et de données associées sur un service Web RESTful, de préférence au format JSON

Ce sera probablement une question stupide mais je vais avoir une de ces nuits. Dans une application, je développe une 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 à comprendre comment cela se passe en une seule demande. Est-il possible de Base64 les données du fichier dans une chaîne JSON? Est-ce que je vais devoir effectuer 2 publications sur le serveur? Ne devrais-je pas utiliser JSON pour cela?

En passant, nous utilisons Grails sur le backend et ces services sont accessibles aux clients mobiles natifs (iPhone, Android, etc.), le cas échéant.

662
Gregg

J'ai posé une question similaire ici:

Comment puis-je 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 POST multipart/form-data et renvoyez un ID au client. Le client envoie ensuite les métadonnées avec l'ID et le serveur associe à nouveau 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 associe à nouveau le fichier et les métadonnées.
553
Daniel T.

Vous pouvez envoyer le fichier et les données en une requête 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 l'entrée de l'utilisateur ou incluses à partir des fichiers sélectionnés par l'utilisateur. Lorsque le formulaire est rempli, les données du formulaire sont envoyées par l'utilisateur à l'application destinataire.

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

De 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 contenu de disposition [RFC 2183] où le type de disposition est "formulaire-données" et où la disposition contient un paramètre (supplémentaire) de "nom", où la valeur de ce paramètre est la valeur d'origine. nom du champ dans le formulaire. Par exemple, une pièce peut contenir un en-tête:

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

avec la valeur correspondant à la saisie du champ "utilisateur".

Vous pouvez inclure des informations de fichier ou de champ dans chaque section entre les limites. J'ai implémenté avec succès un service RESTful qui obligeait l'utilisateur à soumettre des données et un formulaire, et les données multipart/form-fonctionnaient parfaitement. Le service a été créé à l'aide de Java/Spring et le client utilisait C #. Je n'ai donc malheureusement aucun exemple Grails à vous donner concernant la configuration du service. Dans ce cas, vous n'avez pas besoin d'utiliser JSON, car chaque section "données de formulaire" vous fournit un emplacement pour spécifier le nom du paramètre et sa valeur.

L'avantage de l'utilisation de multipart/form-data est que vous utilisez des en-têtes définis par HTTP, vous restez donc fidèle à la philosophie REST consistant à utiliser les outils HTTP existants pour créer votre service.

94
McStretch

Je sais que ce fil est assez ancien, cependant, il me manque une option. Si vous souhaitez envoyer des métadonnées (quel que soit leur format) avec les données à télécharger, vous pouvez effectuer une seule demande multipart/related.

Le type de média Multipart/Related est destiné aux objets composés constitués de plusieurs parties du corps interdépendantes.

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 parties sont identifiées par une chaîne de limite, et la chaîne de limite finale est suivie de deux traits d'union.

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--
42
pgiecek

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 Grails REST Webservices et un client iPhone qui envoient des images, 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 requête 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];

Du côté du 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 cela fonctionne bien dans l'environnement de production.

12
Rscorreia

Voici mon approche API (exemple d'utilisation) - comme vous pouvez le constater, je n'utilise pas d'identificateur de fichier (identifiant de fichier téléchargé sur le serveur) dans l'API:

1.Créez un objet 'photo' sur le serveur:

POST: /projects/{project_id}/photos   
params in: {name:some_schema.jpg, comment:blah}
return: photo_id

Fichier 2.Upload (notez que 'fichier' est au singulier car il n'y en a qu'un par photo):

POST: /projects/{project_id}/photos/{photo_id}/file
params in: file to upload
return: -

Et puis par exemple:

3.Lire la liste des photos

GET: /projects/{project_id}/photos
params in: -
return: array of objects: [ photo, photo, photo, ... ]

4.Lire quelques détails de la photo

GET: /projects/{project_id}/photos/{photo_id}
params in: -
return: photo = { id: 666, name:'some_schema.jpg', comment:'blah'}

5.Lire le fichier photo

GET: /projects/{project_id}/photos/{photo_id}/file
params in: -
return: file content

La conclusion est donc que, tout d'abord, vous créez un objet (photo) par POST, puis vous envoyez une deuxième demande avec fichier (encore une fois POST).

8
Kamil Kiełczewski

Comme le seul exemple manquant est le exemple Android, je vais l'ajouter. Cette technique utilise une AsyncTask personnalisée qui doit être déclarée dans votre classe d'activité.

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

}

Alors, quand vous voulez télécharger votre fichier, appelez simplement:

new UploadFile().execute();
6
lifeisfoo

Objets FormData: Télécharger des fichiers avec Ajax

XMLHttpRequest Level 2 ajoute la prise en charge de la nouvelle interface FormData. Les objets FormData permettent de construire facilement un ensemble de paires clé/valeur représentant les champs de formulaire et leurs valeurs, qui peuvent ensuite être facilement envoyées à 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

5
lakhan_Ideavate

Je voulais envoyer des chaînes au serveur principal. Je n'ai pas utilisé json avec multipart, j'ai utilisé params request.

@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)

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 fichier. J'espère que cela aidera ceux qui n'ont pas les données complexes JSON à envoyer.

1
Aslam anwer
@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
}
0
sunleo