ASP.NET WebApi: Subida de ficheros

Buenas! Vamos a ver en este post como podemos tratar la subida de ficheros en WebApi.

En ASP.NET MVC la subida de ficheros la gestiona un model binder para el tipo HttpFilePostedBase, por lo que basta con declarar un parámetro de este tipo de datos en el controlador y automáticamente recibimos el fichero subido.

En WebApi el enfoque es muy distinto: en el controlador no recibimos ningún parámetro con el contenido del fichero. En su lugar usamos la clase MultipartFormDataStreamProvider para leer el fichero subido y guardarlo en disco (ambas cosas a la vez).

Anatomía de una petición http con fichero subido

Antes de nada veamos como es una petición HTTP en la que se suba un fichero. Para ello he creado un HTML como el siguiente:

  1. <form method="post" enctype="multipart/form-data">
  2.     File: <input type="file" name="aFile"><br />
  3.     File: <input type="file" name="aFile"><br />
  4.     <input type="submit" value="Submit">
  5. </form>

Selecciono dos ficheros cualesquiera y capturo la petición generada con fiddler. El resultado (eliminando todo lo que no nos importa) es el siguiente:

  1. Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryQoYjfxGXTHG6DESL
  2.  
  3. ——WebKitFormBoundaryQoYjfxGXTHG6DESL
  4. Content-Disposition: form-data; name="aFile"; filename="jsio.png"
  5. Content-Type: image/png
  6.  
  7. Contenido binario del fichero
  8. ——WebKitFormBoundaryQoYjfxGXTHG6DESL
  9. Content-Disposition: form-data; name="aFile"; filename="logo_mvp.png"
  10. Content-Type: image/png
  11.  
  12. Contenido binario del fichero
  13. ——WebKitFormBoundaryQoYjfxGXTHG6DESL–

Básicamente:

  • El Content-Type debe ser multipart/form-data
  • El Content-Type debe especificar una boundary. La boundary es un cadena que se usa para separar cada valor de la petición (tanto los ficheros como los valores enviados por formdata si los hubiese).
  • Para cada valor:
    • Se coloca el boundary precedido de —
    • Si es un fichero.
      • se coloca un content-disposition que indica (entre otras cosas) el nombre del fichero
      • El conteido binario del fichero
    • Si no es un fichero (p. ej. es el un formdata que viene de un <input type=text>
      • se coloca un content-disposition que indica el nombre del parámetro
      • Se coloca su valor
  • Finalmente se coloca la boundary para finalizar la petición

Enviar peticiones usando HttpClient

Conociendo como es una petición de subida de ficheros, crearla usando HttpClient es muy simple. El siguiente código sube un fichero:

  1. var requestContent = new MultipartFormDataContent();
  2. var imageContent = new StreamContent(stream);
  3. imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png");
  4. requestContent.Add(imageContent, "image", string.Format("{0:00}.png", idx));

La variable stream es un Stream para acceder al fichero, mientras que la variable idx es un entero que en este caso se usa para dar nombre al fichero subdido (01.png, 02.png, …).

Si capturamos con fiddler como es la petición generada por este código vemos que es como sigue:

  1. POST http://localtest.me:2706/Upload/Photo/568b8c05-aab8-46db-8cbc-aec2a96dec18/2 HTTP/1.1
  2. Content-Type: multipart/form-data; boundary="c609aabb-3872-4d04-a69d-72024c9325a5"
  3. –c609aabb-3872-4d04-a69d-72024c9325a5
  4. Content-Type: image/png
  5. Content-Disposition: form-data; name=image; filename=02.png; filename*=utf-8''02.png

Podemos observar como se ha generado un boundary para nosotros (realmente el valor del boundary no se usa, es solo para separar los campos) y como se genera un Content-Disposition. Es pues una petición equivalente a usar un <input type=”file” /> (cuyo atributo name fuese “image”).

Recibir el fichero en WebApi

Para recibir el fichero subido, necesitamos una acción de un controlador WebApi y usar un MultipartFormDataStreamProvider para guardar el fichero en disco:

  1. var streamProvider = new MultipartFileStreamProvider(uploadFolder);
  2. await Request.Content.ReadAsMultipartAsync(streamProvider);

Este código ya guarda el fichero en el disco. La carpeta usada es la especificada en la variable uploadFolder. De hecho si hubiese varios ficheros subidos a la vez, este código los guarda todos.

En mi caso he enviado una petición con un Content-Disposition cuyo nombre de fichero es 02.png, así que lo suyo sería esperar que en la carpeta especificada por uploadFolder hubiese este fichero. Pero no vais a encontrar ningún fichero llamado así. Por diseño WebApi ignora el valor de Content-Disposition (por temas de seguridad). En su lugar os vais a encontrar con un fichero (o varios) llamados BodyPart y un guid:

image

Por suerte para hacer que WebApi tenga en cuenta el valor del campo Content-Disposition y guarde el fichero con el nombre especificado basta con heredar de MultipartFormDataStreamProvider y reimplementar el método GetLocalFileName:

  1. class MultipartFormDataContentDispositionStreamProvider : MultipartFormDataStreamProvider
  2. {
  3.     public MultipartFormDataContentDispositionStreamProvider(string rootPath) : base(rootPath)
  4.     {
  5.     }
  6.     public MultipartFormDataContentDispositionStreamProvider(string rootPath, int bufferSize) : base(rootPath, bufferSize)
  7.     {
  8.     }
  9.     public override string GetLocalFileName(HttpContentHeaders headers)
  10.     {
  11.         if (headers.ContentDisposition != null)
  12.         {
  13.             return headers.ContentDisposition.FileName;
  14.         }
  15.         return base.GetLocalFileName(headers);
  16.     }
  17. }

Ahora en el controlador instanciamos un objeto MultipartFormDataContentDispositionStreamProvider en lugar del MultipartFormDataStreamProvider y ahora ya se nos guardarán los ficheros con los nombres especificados. Ojo, recuerda que WebApi no hace eso por defecto por temas de seguridad, así que si implementas esta solución valida los nombres de fichero que te envía el cliente.

¡Y ya está! La verdad es que el modelo de WebApi es radicalmente distinto al de ASP.NET MVC pero igual de sencillo y efectivo 😉

Saludos!

2 comentarios en “ASP.NET WebApi: Subida de ficheros”

  1. Me encantan tus artículos son muy buenos, ahora hay cosas que no entiendo como la has podido deducir. Como por ejemplo “no vais a encontrar ningún fichero llamado así. Por diseño WebApi ignora el valor de Content-Disposition” y la solución que nos ofreces es: “Por suerte para hacer que WebApi tenga en cuenta el valor del campo Content-Disposition y guarde el fichero con el nombre especificado basta con heredar de MultipartFormDataStreamProvider y reimplementar el método GetLocalFileName” ¿Como has podido ver esta parte? ¿Abriste el ensamblado para ver que pasaba con la WebApi o como llegaste a esa conclusión?

    1. Buenas Francisco!
      Pues en este caso lo vi… mirando el código fuente de WebApi 🙂
      La verdad es que es una suerte que todo eso sea open source y que podamos mirar el código… porque hay algunas cosillas que deducirlas, no es trivial!!! 😀
      Gracias por tu comentario!! 😉

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *