Azure Blob Storage como unidad de disco con Dokan

Desde que empecé a trabajar con el blob storage de Azure he echado en falta poder montarlo como otra unidad de disco. Sería fantástico poder trabajar con los blobs como si fueran archivos de una memoria USB: examinarlo con el explorador de archivos de Windows, copiar, pegar, etc.

Buscando un poco encontré que hay una API para subir un VHD a Azure como un Page Blob, y montarlo como otra unidad, lo que es una idea muy interesante en algunos escenarios. Por ejemplo, si lo queremos usar como unidad de almacenamiento extendida en la nube. Hay bastante información por ahí sobre cómo hacer esto. La parte mala es que el tamaño máximo del VHD se ha de indicar al crearlo y que tenemos una limitación de tamaño para ese disco vistual de 1 TB, que es el tamaño máximo de un Page Blob. Bueno, 1 TB tampoco está tan mal :) Pero lo que yo quería era poder conectar un container de blobs del storage como otra unidad de disco.

Para trabajar con el Azure storage de una manera similar al explorador de archivos hay diversas herramientas disponibles, tanto gratuitas como de pago. Entre las primeras destaco la Windows Azure Management Tool (MMC) y entre las de pago el Cloud Storage Studio de Cerebrata. Pero ninguna de ellas se integra realmente con el explorador de archivos, como si fuera otra unidad de disco del equipo.

 

Windows Azure MMC

Windows Azure MMC

 

Entonces me topé con Dokan. Dokan es una librería que permite soportar nuevos sistemas de archivos sin necesidad de programar un nuevo driver que trabaje en modo kernel. Dokan se encarga de la parte más engorrosa, y lo mejor de todo es que hay un binding para .NET🙂
Para hacernos un controlador para el nuevo sistema de ficheros tan sólo hemos de escribir una clase que implemente el interfaz DokanOperations. Este interfaz define todos los métodos necesarios para acceso de lectura y escritura a un sistema de archivos. En el código de ejemplo no hay una implementación completa, sólo he implementado los métodos necesarios para tener acceso de lectura de carpetas y archivos. Los métodos para escritura son algo más complicados de implementar, y los dejo fuera del alcance de este artículo. A lo mejor en el futuro saco una segunda parte sobre ello, ya veremos…

Carpetas y containers, archivos y blobs

En primer lugar, recordemos que una cuenta de blob storage se divide en containers, y dentro de los containers hay blobs. Simplificando, un blob es un archivo. En principio, no hay carpetas dentro de los containers, pero el nombre de los blobs puede contener el carácter ‘/’ que la API trata de manera especial, permitiendo tratarlos como si realmente hubiera carpetas. Por su parte, en los métodos de DokanOperations se trabaja con un filename que es una ruta absoluta del tipo (\carpeta1\carpeta2\archivo.extension).

Para trabajar de forma sencilla, en mi implementación de DokanOperations he creado unas clases e interfaces de soporte que transforman estas rutas en objetos del storage. Por ejemplo, la ruta absoluta anterior la transformo en un blob llamado carpeta2/archivo.extension dentro del container carpeta1. O visto de otra manera, será una instancia de la clase CloudBlobFileItem.

 

CloudItem classes and interfaces diagram

Diagrama de clases e interfaces CloudItem

 

Como puedes observar, todos los archivos son del mismo tipo (CloudBlobFileItem) pero las carpetas pueden ser de tres tipos: la raíz (CloudRootItem, que contiene los containers), los containers (CloudContainerItem) y los directorios virtuales (CloudBlobDirectoryItem). El código fuente completo de estas clases está en el archivo de proyecto adjunto, que puedes descargar en el enlace al final del artículo.

El método factoría que utilizo para obtener la instancia correcta a partir de una ruta y un cliente de storage tiene este aspecto:

public static CloudItem GetInstance(string aPath, CloudBlobClient aBlobClient)
        {
            string lNormPath = aPath.Replace('\\', '/').Trim(new char[] { '/' });
            if (string.IsNullOrEmpty(getContainer(lNormPath)))
                return new CloudRootItem(aBlobClient);

            var lContainer = getContainer(lNormPath);
            if (isContainer(lNormPath))
            {
                var lContainerAddress = lContainer;
                return new CloudContainerItem(aBlobClient, lContainerAddress);
            }

            var lBlob = getBlob(lNormPath);
            var lDirRef = aBlobClient.GetContainerReference(lContainer).GetDirectoryReference(lBlob);
            if(lDirRef.Exists())
                return new CloudBlobDirectoryItem(aBlobClient, lContainer, lBlob);
            return new CloudBlobFileItem(aBlobClient, lContainer, lBlob);
        }

Al utilizar esta factoría, tan sólo me he de preocupar de si lo que he recibido es un ICloudFile o un ICloudDirectory para actuar en consecuencia. De esta forma, la implementación de los métodos de DokanOperations es muy limpia y fácil de seguir. Los métodos implementados quedan así:

        public int CreateFile(string filename, FileAccess access, FileShare share, FileMode mode, FileOptions options, DokanFileInfo info)
        {
            var lItem = CloudItemFactory.GetInstance(filename, _client);
            info.IsDirectory = lItem is ICloudDirectory;
            return lItem.Exists() ? 0 : -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int OpenDirectory(string filename, DokanFileInfo info)
        {
            var lItem = CloudItemFactory.GetInstance(filename, _client);
            if (lItem is CloudRootItem) return 0;
            return (lItem is ICloudDirectory && lItem.Exists()) ? 0 : -DokanNet.ERROR_PATH_NOT_FOUND;
        }

        public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info)
        {
            var lItem = CloudItemFactory.GetInstance(filename, _client);
            if (lItem is ICloudFile)
            {
                try
                {
                    var lFileItem = lItem as ICloudFile;
                    using (var fs = lFileItem.FileOpenRead())
                    {
                        fs.Seek(offset, SeekOrigin.Begin);
                        readBytes = (uint)fs.Read(buffer, 0, buffer.Length);
                    }
                    return 0;
                }
                catch { }
            }
            return -1;
        }

        public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info)
        {
            var lItem = CloudItemFactory.GetInstance(filename, _client);
            if (!lItem.Exists())
                return -1;

            var lFileInfo = lItem.GetInformation();
            fileinfo.Attributes = lFileInfo.Attributes;
            fileinfo.CreationTime = lFileInfo.CreationTime;
            fileinfo.FileName = lFileInfo.FileName;
            fileinfo.LastAccessTime=lFileInfo.LastAccessTime;
            fileinfo.LastWriteTime=lFileInfo.LastWriteTime;
            fileinfo.Length = lFileInfo.Length;

            return 0;
        }

        public int FindFiles(string filename, ArrayList files, DokanFileInfo info)
        {
            var lItem = CloudItemFactory.GetInstance(filename, _client);
            if (!(lItem is ICloudDirectory) || !lItem.Exists())
            {
                return -1;
            }

            var lDirItem = lItem as ICloudDirectory;
            files.AddRange(lDirItem.GetFiles().ToArray());
            return 0;
        }

Ahora nos falta un programa principal que registre en el sistema nuestro nuevo sistema de archivos en la nube. Éste aceptará como parámetros la cadena de conexión al storage y la letra de unidad que queramos asignarle.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dokan;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

namespace BlobStorageFS
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 2)
            {
                Console.WriteLine("Parameters: [Storage Connection string] [Drive letter]");
                return;
            }

            var lConnString = args[0];
            var lDriveLetter = args[1][0];

            var lClient = CloudStorageAccount.Parse(lConnString).CreateCloudBlobClient();

            var opt = new DokanOptions();
            opt.DriveLetter = lDriveLetter;
            opt.DebugMode = true;
            opt.UseStdErr = true;
            opt.VolumeLabel = "BlobStorage_" + lDriveLetter;
            DokanNet.DokanMain(opt, new BlobDrive(lClient));
        }
    }
}

Y bien, sólo queda ejecutarlo. Al lanzarlo, vemos cómo aparece una nueva unidad de disco en nuestro PC, que podemos navegar como si se tratara de un disco USB.

 


Nuestra nueva unidad Blob Storage en marcha

Nuestra nueva unidad Blob Storage en marcha

 

¡Y esto es todo! En el pantallazo anterior vemos el resultado de ejecutar el proyecto de ejemplo contra el mismo Development Storage que veíamos antes con la herramienta Windows Azure MMC. Espero que te haya resultado interesante el experimento.
¡Ojo! Como cualquier código que cuelgue por aquí, éste también es altamente mejorable, no ha sido testado a fondo ni es apto para que se utilice en entornos de producción. Use at your own risk! Si tienes cualquier sugerencia, comentario o duda estaré encantado de que lo hablemos🙂

Descargar código de ejemplo: [BlobStorageFS.zip]
Importante: Es un archivo .zip! Has de cambiarle la extensión y descomprimirlo. WordPress.com no permite subir archivos .zip, por lo que he tenido que ponerle una extensión .odt para saltarme la restricción.
Otras descargas: [Dokan] [DokanNet] [Windows Azure MMC]

Hasta la próxima, happy coding!

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

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s