Paso a paso: FileUpload con ASP.NET MVC3 en Azure

Si hay una necesidad habitual al desarrollar una aplicación web esa es que el usuario pueda subir archivos al servidor. Por ejemplo, su foto. Como vamos a ver, hacer esto con ASP.NET MVC3 y Razor es sumamente sencillo, aunque para que la solución funcione al 100% en Azure hay un par de aspectos a tener en cuenta: las limitaciones de una petición HTTP (por tamaño y tiempo) y el balanceo de estas peticiones entre las N instancias de nuestro Web Role.

El ejemplo más sencillo

Vamos a empezar con el ejemplo más sencillo posible: un formulario que nos muestra los archivos que hemos subido al servidor y nos permite subir uno nuevo. En primer lugar, asegúrate de que tienes instalado ASP.NET MVC 3 (Release Candidate 2) y NuGet. Una vez hemos creado una nueva aplicación web ASP.NET MVC3, clicamos con el botón derecho sobre References y seleccionamos Add Library Package Reference.

Add Library Package Reference

En la lista, buscamos el paquete microsoft-web-helpers y lo incluimos en nuestro proyecto.

microsoft-web-helpers

Index.cshtml

Index.cshtml

Ahora, en nuestras vistas podemos utilizar el helper FileUpload, que nos permite insertar de forma muy sencilla un control de subida de ficheros. Creamos una carpeta FileUpload dentro de Views y ahí colocamos nuestra vista Index.cshtml.

El código de la vista es el siguiente:

@{
    ViewBag.Title = "FileUpload";
}

<h2>FileUpload</h2>
[<a href='@Url.Content( "~/FileUpload" )'>refresh</a>]
<h3>Uploaded files:</h3>
<ul>
@foreach(var f in ViewBag.Files){
    <li>@f</li>
}
</ul>

@FileUpload.GetHtml(initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, includeFormTag: true, uploadText: "Upload")
@if (ViewBag.FileUploaded)
{
    <h2>Fileuploaded!</h2>
}

También tenemos que agregar un controlador (~/Controllers/FileUploadController.cs) que se encargue de mostrar los archivos, así como de tratar la subida del archivo:

// FileUploadController.cs
namespace mvcFileUpload.Controllers
{
    public class FileUploadController : Controller
    {
        public ActionResult Index()
        {
            var uploadpath = Server.MapPath("~/App_Data/UploadedFiles");

            ViewBag.FileUploaded = false;
            // nos estan subiendo un archivo?
            foreach (string file in Request.Files)
            {
                var hpf = Request.Files[file] as HttpPostedFileBase;
                if (hpf.ContentLength == 0)
                    continue;
                var savedFileName = Path.Combine(uploadpath, Path.GetFileName(hpf.FileName));
                hpf.SaveAs(savedFileName);
                ViewBag.FileUploaded = true;
            }

            // lista de archivos subidos
            var files = Directory.GetFiles(uploadpath);
            ViewBag.Files = files;

            return View();
        }
    }
}

Como vemos, vamos a almacenar los archivos en la carpeta ~/App_Data/UploadedFiles, por lo que tenemos que crearla para que la aplicación funcione. Ahora sólo nos queda modificar la ruta por defecto en Global.asax.cs para que apunte a nuestro controlador y ya podremos empezar a subir archivos:

// Global.asax.cs
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "FileUpload", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

Subida de archivos simple

Subimos unos cuantos archivos para probar… ¡funciona! Fácil, ¿eh? Bueno, todo va bien hasta que queremos subir un archivo más grande (en mi caso, unos 8 Mb, puede variar según tu configuración). En ese momento, nos saltará una excepción:

FileUpload1_exception

Y es que por defecto el tamaño de las peticiones está limitado, así como su duración. Podemos aumentar estos límites en la configuración (sección HttpRuntime), aunque a partir de determinado umbral empieza a ser peligroso. Además, en Azure el balanceador de carga nos cortará la petición cuando pase 1 minuto, y este valor no se puede cambiar de ninguna manera. ¿Qué hacemos?

Ir por partes

Afortunadamente, existen controles de cliente que permiten subir el archivo por fragmentos de tamaño configurable, programados en HTML5, Flash, Silverlight, etc. Concretamente, voy a utilizar PlUpload (gratuito para uso no comercial, la licencia comercial OEM son sólo 80€). PlUpload selecciona en tiempo de ejecución el runtime más óptimo según las capacidades del navegador del usuario (HTML5, Flash, Silverlight, Gears, etc.).

Vamos a modificar nuestra vista para que utilice PlUpload:

@{
    ViewBag.Title = "FileUploadChunks";
}

<link type="text/css" rel="Stylesheet" media="screen" href="@Url.Content( "~/plupload/css/jquery.ui.plupload.css" )" />
<link type="text/css" rel="Stylesheet" media="screen" href="@Url.Content("~/plupload/css/plupload.queue.css")" />

<script type="text/javascript" src="@Url.Content( "~/plupload/js/gears_init.js" )"></script>
<script type="text/javascript" src="@Url.Content( "~/plupload/js/plupload.full.min.js" )"></script>
<script type="text/javascript" src="@Url.Content( "~/plupload/js/jquery.ui.plupload.min.js")"></script>

<h2>FileUpload</h2>
[<a href='@Url.Content( "~/FileUploadChunks" )'>refresh</a>]
<h3>Uploaded files:</h3>
<ul>
@foreach(var f in ViewBag.Files){
    <li>@f</li>
}
</ul>

<script type="text/javascript">
    // Convert divs to queue widgets when the DOM is ready
    $(function () {
        $("#uploader").plupload({
            // General settings
            runtimes: 'silverlight,flash,html5',
            url: '@Url.Content( "~/FileUploadChunks/UploadChunk" )',
            max_file_size: '10mb',
            chunk_size: '1mb',
            unique_names: false,

            // Flash settings
            flash_swf_url: '/plupload/js/plupload.flash.swf',

            // Silverlight settings
            silverlight_xap_url: '/plupload/js/plupload.silverlight.xap'
        });

    });
</script>

<div id="uploader">
    <p>You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.</p>
</div>

[Disclaimer: No es recomendable meter los tags link y script por ahí en medio. Su lugar es la cabecera, pero lo dejo así para que sea más fácil de seguir.]

Los parámetros que se le indican son bastante auto-explicativos: el orden de preferencia de los runtimes a utilizar, el tamaño de fragmento (chunk) a subir en cada petición, las rutas a los componentes Flash y Silverlight y la URL a la que se van a enviar los fragmentos. Como podemos deducir al ver el código, tenemos que agregar una acción a nuestro controlador para que sea capaz de manejar las peticiones:

// FileUploadController.cs
        [HttpPost]
        public ActionResult UploadChunk(int? chunk, int chunks, string name)
        {
            var fileUpload = Request.Files[0];
            var uploadpath = Server.MapPath("~/App_Data/UploadedFiles");
            chunk = chunk ?? 0;
            using (var fs = new FileStream(Path.Combine(uploadpath, name), chunk == 0 ? FileMode.Create : FileMode.Append))
            {
                var buffer = new byte[fileUpload.InputStream.Length];
                fileUpload.InputStream.Read(buffer, 0, buffer.Length);
                fs.Write(buffer, 0, buffer.Length);
            }
            return Content("chunk uploaded", "text/plain");
        }

El código no tiene mayor misterio: se crea un archivo en disco y se le van agregando los fragmentos a medida que nos llegan.

Subida de archivos con PlUpload

Subida de archivos con PlUpload

En este punto es donde nos encontramos con un problema cuando queremos ejecutar esto en Azure, con varias instancias de un Web Role ejecutando este código. Si cada petición -fragmento- va a una instancia distinta, ¿dónde vamos a obtener el fichero completo?

Blob Storage al rescate

Pues en ningún sitio. Recordemos que en Azure cada petición es independiente, y puede ser atendida por cualquiera de las instancias de Web Role que tengamos activas. Tal como estamos tratando los fragmentos, en el disco de cada instancia del Web Role tendremos una parte del archivo, ya que cada fragmento puede haber ido a parar a un sitio distinto.

Balanceo de carga en Azure

Balanceo de carga en Azure

Ya que no podemos utilizar el disco de cada instancia, tendremos que almacenar el archivo en un lugar accesible por todas las instancias: el Blob Storage. El código de la nueva acción en el controlador queda así.

        public ActionResult Index()
        {
            // lista de blobs subidos
            var container = getContainer();
            var blobs = container.ListBlobs().Select(b => b.Uri.ToString()).ToArray();
            ViewBag.Blobs = blobs;

            return View();
        }

        [HttpPost]
        public ActionResult UploadChunkBlob(int? chunk, int chunks, string name)
        {
            var fileUpload = Request.Files[0];

            chunk = chunk ?? 0;
            var complete = chunk == chunks - 1;

            var lContainer = getContainer();
            var lBlob = lContainer.GetBlockBlobReference(name);

            // Escribir el bloque
            lBlob.PutBlock(
                Convert.ToBase64String(Encoding.Default.GetBytes("chk_" + chunk))
                , fileUpload.InputStream
                , null);

            if (complete)
            {
                var blockNames = new List();
                for (int i = 0; i <= chunk; i++)
                {
                    blockNames.Add(Convert.ToBase64String(Encoding.Default.GetBytes("chk_" + i)));
                }

                lBlob.PutBlockList(blockNames);
            }

            return Content("chunk uploaded", "text/plain");
        }

        private CloudBlobContainer getContainer()
        {
            var storageaccount = CloudStorageAccount.Parse("UseDevelopmentStorage=true");
            var client = storageaccount.CreateCloudBlobClient();
            var container = client.GetContainerReference("uploadedfiles");
            return container;
        }

Un detalle interesante es ver cómo subimos el contenido del blob por bloques -método PutBlock()-, y sólo cuando detectamos que se ha subido el último fragmento (complete==true) hacemos el commit de todos ellos con la llamada a PutBlockList().
También hemos modificado la acción Index, puesto que ahora no lista los archivos de una carpeta sino los blobs que hay en el container de subida de archivos.

Tienes el código fuente de todo esto aquí [fileUpload.zip.odt]. Como WordPress no me deja subir archivos .zip (por mi seguridad y la tuya, dice), le he tenido que poner una extensión .odt. Simplemente tienes que cambiar la extensión por .zip para poder descomprimirlo.

Espero que te haya sido útil. Hasta la próxima! 🙂

Esta entrada fue publicada en Azure, Dev y etiquetada , , , , , , , . Guarda el enlace permanente.

4 respuestas a Paso a paso: FileUpload con ASP.NET MVC3 en Azure

  1. S. Manuel Jiménez dijo:

    Hola Braulio.
    Muchas gracias por este post, la verdad esta breve y claro, es de mucha ayuda para los que estamos iniciando en el mundo de azure.
    Felicidades!!!

  2. Manuel dijo:

    Buenas tardes,

    estoy siguiendo tu tutorial para crear la subida de archivo y al ejecutarlo me salta un error. El error es el siguiente : «FileUpload no existe en el contexto actual», en referencia a la linea del index.cshtml : «@FileUpload.GetHtml(initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, includeFormTag: true, uploadText: «Upload»)». Podrías echarme una mano?? Gracias

  3. Carlos dijo:

    si tienes el visual en español,puede que ese sea el problema por que el release solo está en ingles y no funciona con el visual en español, estoy buscandolo en español

Replica a Braulio Megías Cancelar la respuesta