servicios servicio restful metodo entre ejemplo diferencia consumir cliente web-services json rest file-upload

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



postman (5)

Tengo un servicio web REST que actualmente expone esta URL:

http://server/data/media

donde los usuarios pueden POST el siguiente JSON:

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

para crear nuevos metadatos de medios.

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

También está usando multipart/form-data como lo que un formulario HTML enviaría, pero estoy usando un servicio web REST y quiero seguir usando JSON si es posible.


El hecho de que no esté envolviendo todo el cuerpo de la solicitud en JSON, no significa que no sea REST. Usar multipart/form-data para publicar tanto el JSON como el archivo (o varios archivos) en una sola solicitud:

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

en el lado del servidor (usando Python como la lengua lingua franca de programación aquí):

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 usar "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] y request.args[''file2''][0]

O reutiliza la misma para muchos:

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

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

o realmente pasar varios archivos en un solo campo de una sola vez:

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

... en cuyo caso, request.args[''files''] será una cadena que contendrá todos los archivos, que deberá analizar usted mismo, sin estar seguro de cómo hacerlo, pero estoy seguro de que no es difícil, o mejor sólo tiene que utilizar los enfoques anteriores.

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

PS. Solo porque estoy usando curl como una forma de generar las solicitudes POST no significa que no se puedan enviar exactamente las mismas solicitudes HTTP desde un lenguaje de programación como Python o usando una herramienta suficientemente capaz.


Estoy de acuerdo con Greg en que un enfoque de dos fases es una solución razonable, sin embargo, 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" }

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 otro servidor con más espacio / capacidad. O puede implementar algún tipo de enfoque de round robin si el ancho de banda es un problema.


Me doy cuenta de que esta es una pregunta muy antigua, pero espero que esto ayude a alguien más cuando llegué a esta publicación en busca de lo mismo. Tuve un problema similar, solo que mis metadatos eran un Guid e int. Sin embargo, la solución es la misma. Solo puede hacer que los metadatos necesarios formen parte de la URL.

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

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude) { //See http://.com/a/10327789/431906 for how to accept a file return null; }

Entonces, en lo que sea que esté registrando rutas, WebApiConfig.Register (HttpConfiguration config) para mí en este caso.

config.Routes.MapHttpRoute( name: "FooController", routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}", defaults: new { } );


Si su archivo y sus metadatos crean un recurso, está perfectamente bien cargarlos en una sola solicitud. La solicitud de muestra sería:

POST https://target.com/myresources/resourcename HTTP/1.1 Accept: application/json Content-Type: multipart/form-data; boundary=-----------------------------28947758029299 Host: target.com -------------------------------28947758029299 Content-Disposition: form-data; name="application/json" {"markers": [ { "point":new GLatLng(40.266044,-74.718479), "homeTeam":"Lawrence Library", "awayTeam":"LUGip", "markerImage":"images/red.png", "information": "Linux users group meets second Wednesday of each month.", "fixture":"Wednesday 7pm", "capacity":"", "previousScore":"" }, { "point":new GLatLng(40.211600,-74.695702), "homeTeam":"Hamilton Library", "awayTeam":"LUGip HW SIG", "markerImage":"images/white.png", "information": "Linux users can meet the first Tuesday of the month to work out harward and configuration issues.", "fixture":"Tuesday 7pm", "capacity":"", "tv":"" }, { "point":new GLatLng(40.294535,-74.682012), "homeTeam":"Applebees", "awayTeam":"After LUPip Mtg Spot", "markerImage":"images/newcastle.png", "information": "Some of us go there after the main LUGip meeting, drink brews, and talk.", "fixture":"Wednesday whenever", "capacity":"2 to 4 pints", "tv":"" }, ] } -------------------------------28947758029299 Content-Disposition: form-data; name="name"; filename="myfilename.pdf" Content-Type: application/octet-stream %PDF-1.4 % 2 0 obj <</Length 57/Filter/FlateDecode>>stream x+r 26S00SI2P0Qn F !i/ )%[email protected] [ endstream endobj 4 0 obj <</Type/Page/MediaBox[0 0 595 842]/Resources<</Font<</F1 1 0 R>>>>/Contents 2 0 R/Parent 3 0 R>> endobj 1 0 obj <</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>> endobj 3 0 obj <</Type/Pages/Count 1/Kids[4 0 R]>> endobj 5 0 obj <</Type/Catalog/Pages 3 0 R>> endobj 6 0 obj <</Producer(iTextSharp 5.5.11 2000-2017 iText Group NV /(AGPL-version/))/CreationDate(D:20170630120636+02''00'')/ModDate(D:20170630120636+02''00'')>> endobj xref 0 7 0000000000 65535 f 0000000250 00000 n 0000000015 00000 n 0000000338 00000 n 0000000138 00000 n 0000000389 00000 n 0000000434 00000 n trailer <</Size 7/Root 5 0 R/Info 6 0 R/ID [<c7c34272c2e618698de73f4e1a65a1b5><c7c34272c2e618698de73f4e1a65a1b5>]>> %iText-5.5.11 startxref 597 %%EOF -------------------------------28947758029299--


Una forma de abordar el problema es hacer que la carga sea un proceso de dos fases. Primero, cargaría el archivo utilizando un 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" }

La inclusión de la base de datos de archivos 64 codificada en la solicitud JSON aumentará el tamaño de los datos transferidos en un 33%. Esto puede o no ser importante dependiendo del tamaño general del archivo.

Otro enfoque podría ser utilizar un POST de los datos de archivo sin procesar, pero incluir cualquier metadato en el encabezado de solicitud HTTP. Sin embargo, esto cae un poco fuera de las operaciones REST básicas y puede ser más incómodo para algunas bibliotecas de clientes HTTP.