Por norma general, he visto que la forma más común de subir archivos a blobs de Microsoft Azure Storage es haciendo una petición al servidor web que hemos desarrollado y una vez que tenemos el archivo en este, comenzamos la subida a la cuenta de Storage. Si los archivos no tienen un tamaño excesivamente grande puede ser suficiente. Sin embargo, si queremos subir elementos de un tamaño considerable podemos mejorar el proceso subiendo los archivos directamente desde el lado del cliente a la cuenta de Storage. Para ello debemos seguir una series de pasos: Habilitar CORS en la cuenta de Storage, generar un token para el acceso y por último realizar la subida a través de una petición AJAX. En este post veremos cada uno de los pasos.
Habilitar CORS para el servicio de Blobs
Hace algún tiempo, habilitar CORS en una cuenta de Azure Storage no era posible y lo que se hacía era ubicar el código HTML que se encargaba de la subida dentro de la propia cuenta de Storage, con el objetivo de que el mismo estuviera bajo el mismo origen que la cuenta de almacenamiento. Ahora si es posible habilitar CORS a nivel de Blobs y Tables, de la siguiente manera:
using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Shared.Protocol; using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Web; namespace UploadBlobsJavaScript { public class StorageCORSConfig { public static void RegisterDomains() { //1. Install-Package WindowsAzure.Storage //2. Get Storage context var account = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageAccount"].ConnectionString); //3. Create a blob client var blobClient = account.CreateCloudBlobClient(); // 4. Get the current service properties ServiceProperties blobServiceProperties = blobClient.GetServiceProperties(); //5. Create a new CORS properties configuration blobServiceProperties.Cors = new CorsProperties(); //6. Add CORS rules blobServiceProperties.Cors.CorsRules.Add(new CorsRule() { AllowedHeaders = new List<string>() { "*" }, AllowedMethods = CorsHttpMethods.Put | CorsHttpMethods.Get | CorsHttpMethods.Head | CorsHttpMethods.Post, AllowedOrigins = new List<string>() { "http://corsazurestorage.azurewebsites.net" }, ExposedHeaders = new List<string>() { "*" }, MaxAgeInSeconds = 1800 // 30 minutes }); blobClient.SetServiceProperties(blobServiceProperties); } } }
Básicamente lo que hacemos es recuperar las propiedades del servicio de blobs de nuestra cuenta y crear una nueva configuración para el apartado CORS. Este se basa en reglas, donde podemos determinar qué orígenes tienen permitido realizar peticiones, incluso los Http Verbs disponibles. En este ejemplo sólo permito el acceso desde http://corsazurestorage.azurewebsites.net.
El método anterior es llamado desde el Global.asax del sitio para que esté disponible desde el primer momento:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace UploadBlobsJavaScript { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); StorageCORSConfig.RegisterDomains(); } } }
Una forma rápida de comprobar que CORS está activado y que se ha aplicado la regla que hemos creado es usando Azure Storage Explorer:
Seleccionando dentro del listado la rama Blob Containers (N) aparecerá la opción CORS con un check. Si hacemos clic justo encima podremos ver las reglas, eliminarlas y modificarlas:
Recuperar una Shared Access Signature para el nuevo blob
Para poder tener acceso tanto de escritura como de lectura de los recursos de la cuenta es necesario generar un token que se adjuntará durante la petición:
using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using System; using System.Configuration; using System.Globalization; using System.Web.Http; namespace UploadBlobsJavaScript.Controllers { public class StorageController : ApiController { public string GetSaS(string containerName, string blobName) { //1. Nuget: Install-Package WindowsAzure.Storage //2. Get context account var account = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageAccount"].ConnectionString); //3. Create a blob client var blobClient = account.CreateCloudBlobClient(); //4. Get a container and create it if not exists var container = blobClient.GetContainerReference(containerName); container.CreateIfNotExists(); //5. Get a blob reference CloudBlockBlob blob = container.GetBlockBlobReference(blobName); //6. Create a Shared Access Signature for the blob var SaS = blob.GetSharedAccessSignature( new SharedAccessBlobPolicy() { Permissions = SharedAccessBlobPermissions.Write, SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(30), }); return string.Format(CultureInfo.InvariantCulture, "{0}{1}", blob.Uri, SaS); } } }
En este código tenemos un método llamado GetSaS que recibe el nombre del container y el nombre del blob que queremos crear. Una vez que tenemos la referencia del blob creamos el token, en este caso sólo de escritura, con un tiempo de expiración de 30 minutos.
Subida de archivos desde JavaScript
Para comprobar la configuración con un ejemplo, he utilizado la siguiente página:
@{ ViewBag.Title = "Home Page"; } <div class="jumbotron"> <h2>Upload files to Microsoft Azure Storage using JavaScript</h2> </div> <div class="container"> <div class="row"> <div class="form-group"> <label for="ContainerName">Container name:</label> <input type="text" class="form-control" id="ContainerName" placeholder="Enter a container name"> </div> <div class="form-group"> <label for="Files"></label> <input type="file" id="fileControl" multiple /> <progress id="uploadProgress" class="form-control" value="0" max="100"></progress> </div> <div class="form-group"> <input type="button" id="btnUpload" value="Upload files" /> </div> </div> </div> @section scripts{ <script> function upload(file, type, url) { var ajaxRequest = new XMLHttpRequest(); ajaxRequest.onreadystatechange = function (aEvt) { console.log(ajaxRequest.readyState); if (ajaxRequest.readyState == 4) console.log(ajaxRequest.responseText); }; ajaxRequest.upload.onprogress = function (e) { var percentComplete = (e.loaded / e.total) * 100; console.log(percentComplete + "% completed"); uploadProgress.value = percentComplete; }; ajaxRequest.onerror = function () { alert("ajaxRequest error"); }; ajaxRequest.open('PUT', url, true); ajaxRequest.setRequestHeader('Content-Type', type); ajaxRequest.setRequestHeader('x-ms-blob-type', 'BlockBlob'); ajaxRequest.send(file); } $("#btnUpload").click(function () { var files = fileControl.files; for (var i = 0, file; file = files[i]; i++) { var reader = new FileReader(); reader.onloadend = (function (theFile) { return function (e) { $.ajax({ type: 'GET', url: '/api/storage/getsas?containerName=' + $("#ContainerName").val() + '&blobName=' + theFile.name, success: function (res, status, xhr) { upload(e.target.result, theFile.type, res); }, error: function (res, status, xhr) { alert("Can't get the Shared Access Signature"); } }); }; })(file); reader.readAsArrayBuffer(file); } }); </script> }
En ella recupero el nombre del container, el archivo que quiero subir (puedo subir varios a la vez) y un botón para comenzar la subida. En el código JavaScript recorro y leo cada uno de los archivos seleccionados y realizo una llamada a /api/storage/getSaS por cada uno, con el nombre del container y del blob. Una vez obtengo la URL donde debo realizar la petición de subida, hago una llamada al método upload, donde se manejan una serie de eventos de XMLHttpRequest y se realiza una llamada a través del verbo PUT con las cabeceras Content-Type y x-ms-blob-type y el archivo.
Espero que sea de utilidad.
¡Saludos!