Publicar un archivo y datos asociados en un servicio web RESTful preferiblemente como JSON

757

Probablemente sea una pregunta estúpida, pero tengo una de esas noches. En una aplicación estoy desarrollando RESTful API y queremos que el cliente envíe datos como JSON. Parte de esta aplicación requiere que el cliente cargue un archivo (generalmente una imagen), así como información sobre la imagen.

Me resulta difícil rastrear cómo sucede esto en una sola solicitud. ¿Es posible Base64 los datos del archivo en una cadena JSON? ¿Voy a necesitar realizar 2 publicaciones en el servidor? ¿No debería estar usando JSON para esto?

Como nota al margen, estamos utilizando Grails en el back-end y los clientes móviles nativos (iPhone, Android, etc.) acceden a estos servicios, si algo de eso marca la diferencia.

Gregg
fuente
1
Entonces, ¿cuál es la mejor manera de hacer esto?
James111
3
Envíe los metadatos en la cadena de consulta de URL, en lugar de JSON.
jrc

Respuestas:

632

Hice una pregunta similar aquí:

¿Cómo subo un archivo con metadatos usando un servicio web REST?

Básicamente tienes tres opciones:

  1. Base64 codifica el archivo, a expensas de aumentar el tamaño de los datos en aproximadamente un 33%, y agrega una sobrecarga de procesamiento tanto en el servidor como en el cliente para la codificación / decodificación.
  2. Envíe el archivo primero en una multipart/form-dataPOST y devuelva una identificación al cliente. Luego, el cliente envía los metadatos con la ID y el servidor vuelve a asociar el archivo y los metadatos.
  3. Envíe los metadatos primero y devuelva una identificación al cliente. El cliente luego envía el archivo con la ID y el servidor vuelve a asociar el archivo y los metadatos.
Daniel T.
fuente
29
Si elegí la opción 1, ¿acabo de incluir el contenido de Base64 dentro de la cadena JSON? {archivo: '234JKFDS # $ @ # $ MFDDMS ....', nombre: 'somename' ...} ¿O hay algo más?
Gregg
15
Gregg, exactamente como has dicho, simplemente lo incluirías como una propiedad, y el valor sería la cadena codificada en base64. Este es probablemente el método más fácil, pero puede no ser práctico dependiendo del tamaño del archivo. Por ejemplo, para nuestra aplicación, necesitamos enviar imágenes de iPhone de 2-3 MB cada una. Un aumento del 33% no es aceptable. Si solo envía imágenes pequeñas de 20 KB, esa sobrecarga podría ser más aceptable.
Daniel T.
19
También debo mencionar que la codificación / decodificación base64 también tomará algo de tiempo de procesamiento. Puede ser lo más fácil de hacer, pero ciertamente no es lo mejor.
Daniel T.
8
json con base64? hmm .. Estoy pensando en apegarme a multiparte / formulario
Omnipresente
12
¿Por qué se niega a usar multipart / form-data en una solicitud?
1nstinct
107

Puede enviar el archivo y los datos en una sola solicitud utilizando el tipo de contenido multipart / form-data :

En muchas aplicaciones, es posible que a un usuario se le presente un formulario. El usuario completará el formulario, incluida la información escrita, generada por la entrada del usuario o incluida en los archivos que el usuario ha seleccionado. Cuando se completa el formulario, los datos del formulario se envían desde el usuario a la aplicación receptora.

La definición de MultiPart / Form-Data se deriva de una de esas aplicaciones ...

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

"multipart / form-data" contiene una serie de partes. Se espera que cada parte contenga un encabezado de disposición de contenido [RFC 2183] donde el tipo de disposición es "datos de formulario", y donde la disposición contiene un parámetro (adicional) de "nombre", donde el valor de ese parámetro es el original nombre del campo en el formulario. Por ejemplo, una parte puede contener un encabezado:

Disposición de contenido: datos de formulario; nombre = "usuario"

con el valor correspondiente a la entrada del campo "usuario".

Puede incluir información de archivo o información de campo dentro de cada sección entre límites. Implementé con éxito un servicio RESTful que requería que el usuario enviara datos y un formulario, y multipart / form-data funcionó perfectamente. El servicio se creó con Java / Spring, y el cliente usaba C #, por lo que desafortunadamente no tengo ningún ejemplo de Grails que pueda brindarle sobre cómo configurar el servicio. No necesita utilizar JSON en este caso ya que cada sección de "datos de formulario" le proporciona un lugar para especificar el nombre del parámetro y su valor.

Lo bueno de usar datos multiparte / formulario es que está usando encabezados definidos por HTTP, por lo que se apega a la filosofía REST de usar herramientas HTTP existentes para crear su servicio.

McStretch
fuente
1
Gracias, pero mi pregunta se centró en querer usar JSON para la solicitud y si eso fuera posible. Ya sé que podría enviarlo de la manera que sugieres.
Gregg
15
Sí, esa es esencialmente mi respuesta para "¿No debería estar usando JSON para esto?" ¿Hay alguna razón específica por la que desea que el cliente use JSON?
McStretch el
3
Lo más probable es que sea un requisito comercial o que se mantenga con coherencia. Por supuesto, lo ideal es aceptar ambos (datos de formulario y respuesta JSON) basados ​​en el encabezado HTTP Content-Type.
Daniel T.
2
Elegir JSON da como resultado un código mucho más elegante tanto en el lado del cliente como del servidor, lo que conduce a errores menos potenciales. Los datos del formulario son tan ayer.
superarts.org
55
Pido disculpas por lo que dije si lastima la sensación de algunos desarrolladores de .Net. Aunque el inglés no es mi lengua materna, no es una excusa válida para mí decir algo grosero sobre la tecnología en sí. El uso de datos de formulario es increíble y si lo sigue usando, ¡también lo será aún más!
superarts.org
53

Sé que este hilo es bastante antiguo, sin embargo, me falta una opción aquí. Si tiene metadatos (en cualquier formato) que desea enviar junto con los datos para cargar, puede hacer una sola multipart/relatedsolicitud.

El tipo de medio Multipart / Related está destinado a objetos compuestos que consisten en varias partes del cuerpo interrelacionadas.

Puede consultar la especificación RFC 2387 para obtener detalles más detallados.

Básicamente, cada parte de dicha solicitud puede tener contenido con un tipo diferente y todas las partes están relacionadas de alguna manera (por ejemplo, una imagen y sus metadatos). Las partes se identifican por una cadena de límite, y la cadena de límite final es seguida por dos guiones.

Ejemplo:

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
fuente
Me gustó mucho tu solución. Desafortunadamente, parece que no hay forma de crear solicitudes mutlipart / relacionadas en un navegador.
Petr Baudis
¿tiene alguna experiencia en hacer que los clientes (especialmente los de JS) se comuniquen con la API de esta manera
pvgoddijn
desafortunadamente, actualmente no hay un lector para este tipo de datos en php (7.2.1) y tendría que construir su propio analizador
rocío
Es triste que los servidores y los clientes no tengan un buen soporte para esto.
Nader Ghanbari
14

Sé que esta pregunta es antigua, pero en los últimos días había buscado en toda la web para resolver esta misma pregunta. Tengo servicios web REST y cliente de iPhone que envían imágenes, títulos y descripciones.

No sé si mi enfoque es el mejor, pero es tan fácil y simple.

Tomo una foto usando el UIImagePickerController y envío al servidor los NSData usando las etiquetas de encabezado de solicitud para enviar los datos de la imagen.

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];

En el lado del servidor, recibo la foto usando el código:

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

}

No sé si tengo problemas en el futuro, pero ahora funciona bien en el entorno de producción.

Rscorreia
fuente
1
Me gusta esta opción de usar encabezados http. Esto funciona especialmente bien cuando existe cierta simetría entre los metadatos y los encabezados http estándar, pero obviamente puede inventar el suyo.
EJ Campbell
14

Aquí está mi API de enfoque (uso ejemplo): como puede ver, no uso ninguno file_id(identificador de archivo cargado en el servidor) en la API:

  1. Crear photoobjeto en el servidor:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Cargar archivo (tenga fileen cuenta que está en forma singular porque es solo uno por foto):

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

Y luego, por ejemplo:

  1. Leer lista de fotos

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Leer algunos detalles de la foto

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Leer archivo de foto

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

Entonces, la conclusión es que, primero crea un objeto (foto) por POST, y luego envía una segunda solicitud con el archivo (nuevamente POST).

Kamil Kiełczewski
fuente
3
Esta parece ser la forma más 'RESTÍFICA' de lograr esto.
James Webster
La operación POST para los recursos recién creados, debe devolver el id de ubicación, en detalles simples de la versión del objeto
Ivan Proskuryakov
@ivanproskuryakov ¿por qué "debe"? En el ejemplo anterior (POST en el punto 2) la identificación del archivo es inútil. Segundo argumento (para POST en el punto 2) uso la forma singular '/ file' (no '/ files'), por lo que no se necesita ID porque la ruta: / projects / 2 / photos / 3 / file proporciona información COMPLETA al archivo de foto de identidad.
Kamil Kiełczewski
De la especificación del protocolo HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Creado "El recurso recién creado puede ser referenciado por los URI devueltos en la entidad de la respuesta, con el URI más específico para el recurso dado por un campo de encabezado de ubicación ". @ KamilKiełczewski (uno) y (dos) podrían combinarse en una operación POST POST: / projects / {project_id} / photos Le devolverá el encabezado de ubicación, que podría usarse para GET operación de una sola foto (recurso *) GET: para obtener un foto única con todos los detalles CGET: para obtener toda la colección de las fotos
Ivan Proskuryakov
1
Si los metadatos y la carga son operaciones separadas, los puntos finales tienen estos problemas: Para la carga de archivos se utiliza la operación POST: la POST no es idempotente. PUT (idempotente) debe usarse ya que está cambiando el recurso sin crear uno nuevo. REST trabaja con objetos llamados recursos . POST: “../photos/“ PUT: “../photos/{photo_id}” GET: “../photos/“ GET: “../photos/{photo_id}” PS. La carga separada en un punto final separado puede conducir a un comportamiento imprevisto. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

Objetos FormData: cargar archivos usando Ajax

XMLHttpRequest Level 2 agrega soporte para la nueva interfaz FormData. Los objetos FormData proporcionan una manera de construir fácilmente un conjunto de pares clave / valor que representan campos de formulario y sus valores, que luego pueden enviarse fácilmente utilizando el método 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
fuente
6

Como el único ejemplo que falta es el ejemplo de ANDROID , lo agregaré. Esta técnica utiliza un AsyncTask personalizado que debe declararse dentro de su clase de Actividad.

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

}

Entonces, cuando quieras subir tu archivo solo llama:

new UploadFile().execute();
lifeisfoo
fuente
Hola, ¿qué es AndroidMultiPartEntity? Por favor explique ... y si quiero cargar un archivo pdf, word o xls lo que tengo que hacer, brinde alguna guía ... Soy nuevo en esto.
amit pandya
1
@amitpandya He cambiado el código a una carga genérica de archivos para que sea más claro para cualquiera que lo lea
lifeisfoo
2

Quería enviar algunas cadenas al servidor de fondo. No utilicé json con multiparte, utilicé parámetros de solicitud.

@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 se vería así

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

Estoy pasando dos parámetros (uuid y type) junto con la carga del archivo. Espero que esto ayude a quienes no tienen los complejos datos json para enviar.

Aslam anwer
fuente
1

Puede intentar usar https://square.github.io/okhttp/ library. Puede configurar el cuerpo de la solicitud en multiparte y luego agregar el archivo y los objetos json por separado de la siguiente manera:

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
fuente
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
fuente
-5

Asegúrese de tener la siguiente importación. Por supuesto, otras importaciones estándar

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
fuente
1
Este getjava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz