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

Resuelto Daniel T. asked hace 13 años • 7 respuestas

Tengo un servicio web REST que actualmente expone esta URL:

http://servidor/datos/medios

donde los usuarios pueden POSTel siguiente JSON:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

para crear nuevos metadatos de medios.

Ahora necesito poder cargar un archivo al mismo tiempo que los metadatos multimedia. ¿Cuál es la mejor manera de hacer esto? Podría introducir una nueva propiedad llamada filey codificar en base64 el archivo, pero me preguntaba si había una manera mejor.

También se usa multipart/form-datalo que enviaría un formulario HTML, pero estoy usando un servicio web REST y quiero seguir usando JSON si es posible.

Daniel T. avatar Oct 15 '10 07:10 Daniel T.
Aceptado

Estoy de acuerdo con Greg en que un enfoque de dos fases es una solución razonable; sin embargo, yo lo haría al revés. Yo lo haría:

POST http://server/data/media
body:
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

Para crear la entrada de metadatos y devolver una respuesta como:

201 Created
Location: http://server/data/media/21323
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentUrl": "http://server/data/media/21323/content"
}

Luego, el cliente puede usar este ContentUrl y hacer un PUT con los datos del archivo.

Lo bueno de este enfoque es que cuando su servidor comienza a sobrecargarse con inmensos volúmenes de datos, la URL que devuelve puede apuntar a algún otro servidor con más espacio/capacidad. O podría implementar algún tipo de enfoque circular si el ancho de banda es un problema.

Darrel Miller avatar Oct 15 '2010 01:10 Darrel Miller

El hecho de que no esté envolviendo todo el cuerpo de la solicitud en JSON no significa que no sea RESTful utilizar multipart/form-datapara publicar tanto el JSON como los archivos en una sola solicitud:

curl -F "metadata=<metadata.json" -F "[email protected]" http://example.com/add-file

en el lado del servidor :

class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

Para cargar varios archivos, es posible utilizar "campos de formulario" separados para cada uno:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

...en cuyo caso el código del servidor tendrá request.args['file1'][0]yrequest.args['file2'][0]

o reutilizar el mismo para muchos:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

...en cuyo caso request.args['files']será simplemente una lista de longitud 2.

o pasar varios archivos a través de un solo campo:

curl -F "metadata=<metadata.json" -F "[email protected],some-other-file.tar.gz" http://example.com/add-file

...en cuyo caso request.args['files']habrá una cadena que contiene todos los archivos, que tendrás que analizar tú mismo; no estoy seguro de cómo hacerlo, pero estoy seguro de que no es difícil, o mejor simplemente usa los métodos anteriores.

La diferencia entre @y <es que @hace que el archivo se adjunte como una carga de archivo, mientras que <adjunta el contenido del archivo como un campo de texto.

PD: El hecho de que lo esté usando curlcomo una forma de generar las POSTsolicitudes no significa que no se puedan enviar exactamente las mismas solicitudes HTTP desde un lenguaje de programación como Python o utilizando cualquier herramienta suficientemente capaz.

Erik Kaplun avatar Oct 25 '2012 20:10 Erik Kaplun

Una forma de abordar el problema es hacer que la carga sea un proceso de dos fases. Primero, cargaría el archivo mediante una POST, donde el servidor devuelve algún identificador al cliente (un identificador podría ser el SHA1 del contenido del archivo). Luego, una segunda solicitud asocia los metadatos con los datos del archivo:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}

Incluir el archivo de datos base64 codificado en la solicitud JSON aumentará el tamaño de los datos transferidos en un 33%. Esto puede ser importante o no dependiendo del tamaño total del archivo.

Otro enfoque podría ser utilizar una POST de los datos del archivo sin procesar, pero incluir los metadatos en el encabezado de la solicitud HTTP. Sin embargo, esto queda un poco fuera de las operaciones REST básicas y puede resultar más complicado para algunas bibliotecas cliente HTTP.

Greg Hewgill avatar Oct 15 '2010 00:10 Greg Hewgill

No entiendo por qué, a lo largo de ocho años, nadie ha publicado la respuesta fácil. En lugar de codificar el archivo como base64, codifique el json como una cadena. Luego simplemente decodifica el json en el lado del servidor.

En Javascript:

let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

PUBLICARLO usando el tipo de contenido: multipart/form-data

En el lado del servidor, recupere el archivo normalmente y recupere el json como una cadena. Convierta la cadena en un objeto, que suele ser una línea de código sin importar el lenguaje de programación que utilice.

(Sí, funciona muy bien. Haciéndolo en una de mis aplicaciones).

ccleve avatar May 17 '2019 22:05 ccleve

Me doy cuenta de que esta es una pregunta muy antigua, pero espero que ayude a alguien más, ya que encontré esta publicación buscando lo mismo. Tuve un problema similar, solo que mis metadatos eran Guid e int. Aunque la solución es la misma. Puede simplemente hacer que los metadatos necesarios formen parte de la URL.

Método de aceptación POST en su clase "Controlador":

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

Luego, en cualquier ruta que esté registrando, WebApiConfig.Register(HttpConfiguration config) para mí en este caso.

config.Routes.MapHttpRoute(
    name: "FooController",
    routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}",
    defaults: new { }
);
Greg Biles avatar Feb 25 '2013 16:02 Greg Biles