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

249

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 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 filey base64 codificar el archivo, pero me preguntaba si había una mejor manera.

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

Daniel T.
fuente
35
En realidad, no es necesario seguir utilizando JSON para tener un servicio web RESTful. REST es básicamente todo lo que sigue los principios principales de los métodos HTTP y algunas otras reglas (posiblemente no estandarizadas).
Erik Kaplun

Respuestas:

192

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 ser pesado con inmensos volúmenes de datos, la url que devuelve puede apuntar a otro servidor con más espacio / capacidad. O podría implementar algún tipo de enfoque round robin si el ancho de banda es un problema.

Darrel Miller
fuente
8
Una ventaja de enviar el contenido primero es que para cuando existen los metadatos, el contenido ya está presente. En última instancia, la respuesta correcta depende de la organización de los datos en el sistema.
Greg Hewgill
Gracias, marqué esto como la respuesta correcta porque esto es lo que quería hacer. Desafortunadamente, debido a una extraña regla comercial, tenemos que permitir que la carga ocurra en cualquier orden (metadatos primero o archivo primero). Me preguntaba si había una forma de combinar los dos para evitar el dolor de cabeza de lidiar con ambas situaciones.
Daniel T.
@Daniel Si publica el archivo de datos primero, puede tomar la URL devuelta en Ubicación y agregarla al atributo ContentUrl en los metadatos. De esa manera, cuando el servidor recibe los metadatos, si existe un ContentUrl, ya sabe dónde está el archivo. Si no hay ContentUrl, entonces sabe que debería crear uno.
Darrel Miller
si fuera a hacer la POST primero, ¿publicaría en la misma URL? (/ server / data / media) o crearía otro punto de entrada para las cargas de archivos primero?
Matt Brailsford
1
@Faraway ¿Qué pasa si los metadatos incluyen el número de "me gusta" de una imagen? ¿Lo tratarías como un único recurso entonces? O, más obviamente, ¿sugiere que si quisiera editar la descripción de una imagen, tendría que volver a cargar la imagen? Hay muchos casos en los que los formularios de varias partes son la solución correcta. Simplemente no es siempre el caso.
Darrel Miller
103

El hecho de que no esté envolviendo todo el cuerpo de la solicitud en JSON, no significa que no sea RESTful usar 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 (usando Python para pseudocódigo):

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

para cargar múltiples 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]yrequest.args['file2'][0]

o reutilice 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']simplemente será una lista de longitud 2.

o pasar múltiples 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']será una cadena que contiene todos los archivos, que tendrá que analizar usted mismo, no estoy seguro de cómo hacerlo, pero estoy seguro de que no es difícil, o mejor simplemente use los enfoques 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: Solo porque lo estoy 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 usando una herramienta suficientemente capaz.

Erik Kaplun
fuente
44
Me había estado preguntando sobre este enfoque, y por qué no había visto a nadie más presentarlo todavía. Estoy de acuerdo, me parece perfectamente RESTful.
sopa de sopa
1
¡SI! Este es un enfoque muy práctico, y no es menos RESTANTE que usar "application / json" como tipo de contenido para toda la solicitud.
sickill
..pero eso solo es posible si tiene los datos en un archivo .json y lo carga, lo cual no es el caso
itsjavi
55
@mjolnic tu comentario es irrelevante: los ejemplos de cURL son solo, bueno, ejemplos ; la respuesta dice explícitamente que puede usar cualquier cosa para enviar la solicitud ... además, ¿qué le impide simplemente escribir curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun
3
Estoy usando este enfoque porque la respuesta aceptada no funcionaría para la aplicación que estoy desarrollando (el archivo no puede existir antes de los datos y agrega una complejidad innecesaria para manejar el caso donde los datos se cargan primero y el archivo nunca se carga) .
BitsEvolved
33

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

La inclusión de la base de datos del archivo 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 usar una POST de los datos del archivo sin formato, pero incluir cualquier metadato en el encabezado de la solicitud HTTP. Sin embargo, esto queda un poco fuera de las operaciones REST básicas y puede ser más incómodo para algunas bibliotecas de cliente HTTP.

Greg Hewgill
fuente
Puede usar Ascii85 aumentando solo 1/4.
Singagirl
¿Alguna referencia sobre por qué base64 aumenta tanto el tamaño?
jam01
1
@ jam01: Casualmente, ayer vi algo que responde bien a la pregunta de espacio: ¿Cuál es la sobrecarga de espacio de la codificación Base64?
Greg Hewgill
10

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 buscando lo mismo. Tuve un problema similar, solo que mis metadatos eran Guid e int. Sin embargo, la solución es la misma. Simplemente puede 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 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 { }
);
Greg Biles
fuente
5

Si su archivo y sus metadatos crean un recurso, está 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\
)%!Y0i@.k
[
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--
Mike Ezzati
fuente
3

No entiendo por qué, en el transcurso 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 decodifique el json en el lado del servidor.

En Javascript:

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

Publíquelo usando Content-Type: 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 generalmente es una línea de código, sin importar el lenguaje de programación que use.

(Sí, funciona muy bien. Hacerlo en una de mis aplicaciones).

ccleve
fuente