最近我全身心的投入到我们第一个基于云的平台-XLR8- (研发代码: Xalent)的工作中。一周前,我们的首席架构师, Ray,让我试着将该平台部署至Windows Azure上。我们需要对平台做一些修改,其中之一便是Windows Azure不能使用本地文件系统来存储任何最终用户上传的文件。原因有2个:

  • 所有web role 项目下的文件会被当做一个程序包。这意味着当我们部署web role时,Windows Azure会删除原有的文件夹和文件,然后展开新的程序包,并进行初始化工作。因此所有用户上传的文件此时都会被删除。
  • 在某些情况下,Windows Azure 平台会将您的应用从一个虚拟机搬移至另外一个。 我们无法确保应用根路径的一致性。所以对于useServer.Mappath() ,它会返回不同的结果。

因此,当应用部署至Windows Azure时,对于上传的文件最好将其存储在Windows Azure Blob storage 中。

 

难题和目标

当我们将一般web应用搬移至Windows Azure时,我们需要修改所有上传文件相关的代码,甚至是显式图片的代码。我面临的问题是web应用应能同时满足Windows Azure 和一般的部署环境的情况。这意味着当其部署至Windows Azure 或一般服务器时,我们不应该在业务逻辑层和UI层去修改文件操作代码。我们要确保代码在2种部署情况下都能正常运行,我们能做的修改仅仅是一些部署配置。

一个解决办法是使用Cloud Drive 特性。那样的话我们可以在Blob挂载一个VHD 文件当做本地硬盘来使用。这样基本无需更改IO操作和代码。但是将文件存储于Blob内会有其他一些优势,例如可以通过URL直接访问文件。

所以难题便是,我需要一个设计模式来负责文件的操作,且无论是一般文件系统还是Blob storage。本文我会介绍一下我是如何处理这些问题的,希望对读者在未来开发Windows Azure 和一般web应用时有所帮助。 

简单的架构和实现

整个架构非常简单。为了使得web应用依赖于抽象的文件操作,我创建了一个接口来隔离一般文件系统和Blob storage实现上的差别。

 

在 IFileSystemAgent  接口中,我定义了基本的文件操作方法,例如SaveLoadDelete  和Exist sGetResourceUrl  方法用于访问文件URL,这对于在网页上显示图片来说非常有用。它会基于当前部署的系统返回适当的URL。

public interface IFileSystemAgent

{

    void Save(Stream fileStream, string filename, bool overwrite);

 

    void Save(byte [] bytes, string filename, bool overwrite);

 

    byte [] Load(string filename);

 

    bool Exists(string filename);

 

    void Delete(string filename);

 

    string GetResourceUrl(string filename);

}

 

 

IFileSystemAgent  接口之上我实现了2个类,一个用于一般的Windows系统的文件操作,一个用于Blob storage。  

这2个实现类的区别不仅在于文件操作,还有根路径问题。在web应用中,对于一般的文件系统,我们使用Server.MapPath()  来将虚拟路径转换为物理路径,以便保存和读取文件。但是在Blob storage 中,我们需要获取Blob storage 账户信息,向该账户的端点传输字节或者数据流,这和一般文件系统是非常不同的。

当我们需要在一个网页上显示或链接文件时,在windows文件系统中,我们只需使用相对路径,举例来说: "/upload/images/beijing-hotel-img1_50x50.jpg"。但是在Blob storage中,一般路径如下形式: "http://xlr8.blob.core.windows.net/default/beijing-hotel-img1_50x50.jpg".

因此,当保存或链接文件时, IFileSystemAgent  只接受文件名和相对路径,具体实现类会决定如何以及在哪里存储文件。 

我将 HttpServerUtilityBase  以及一个名为Root的参数传入WindowsFileSystemAgent 的构造函数中。文件必须存储在Server.MapPath("/" + Root)  目录下。在 AzureBlobFileSystemAgent  构造函数中,我同样传入 CloudStorageAccount  以及 ContainerName  ,这样文件便会存储在相应账户的指定容器内。

如下是2个实现类的具体实现。

public class WindowsFileSystemAgent : IFileSystemAgent

{

    private HttpServerUtilityBase _server;

    private string _root;

 

    public HttpServerUtilityBase Server

    {

        get

        {

            return _server;

        }

        set

        {

            _server = value ;

        }

    }

 

    public string Root

    {

        get

        {

            return _root;

        }

        set

        {

            _root = value ;

        }

    }

 

    public WindowsFileSystemAgent()

        : this (null , string .Empty)

    {

    }

 

     public WindowsFileSystemAgent(HttpServerUtilityBase server, string root)

    {

        _server = server;

        _root = root;

    }

 

    private string GetServerSideFullname(string filename)

    {

        return Path.Combine(_server.MapPath("/" + _root), filename);

    }

 

    #region IFileSystemAgent Members

 

    public void Save(Stream fileStream, string filename, bool overwrite)

    {

        byte [] bytes = new byte [fileStream.Length];

        fileStream.Read(bytes, 0, (int )fileStream.Length);

 

        Save(bytes, filename, overwrite);

    }

 

    public void Save(byte [] bytes, string filename, bool overwrite)

    {

        filename = GetServerSideFullname(filename);

        var directory = Path.GetDirectoryName(filename);

        if (!Exists(directory))

        {

            Directory.CreateDirectory(directory);

        }

        if (Exists(filename))

        {

            if (overwrite)

            {

                Delete(filename);

            }

            else

            {

                throw new ApplicationException (string .Format("Existed file {0} please select another name or set the overwrite = true." ));

            }

        }

        using (var stream = File.Create(filename))

        {

            stream.Write(bytes, 0, bytes.Length);

        }

    }

 

    public byte [] Load(string filename)

    {

        filename = GetServerSideFullname(filename);

        byte [] bytes;

        using (var stream = File.OpenRead(filename))

        {

            bytes = new byte [stream.Length];

             stream.Read(bytes, 0, bytes.Length);

        }

        return bytes;

    }

 

    public bool Exists(string filename)

    {

        filename = GetServerSideFullname(filename);

        if (File.Exists(filename))

        {

            return true ;

        }

        else

        {

            return Directory.Exists(filename);

        }

    }

 

    public void Delete(string filename)

    {

        filename = GetServerSideFullname(filename);

        if (File.Exists(filename))

        {

            File.Delete(filename);

        }

    }

 

    public string GetResourceUrl(string filename)

    {

        return "/" + _root + "/" + filename;

    }

 

    #endregion

}

public class AzureBlobFileSystemAgent : IFileSystemAgent

{

    private static string CST_DEFAULTCONTAINERNAME = "default" ;

    private static string CST_DEFAULTACCOUNTSETTING = "DataConnectionString" ;

 

    private string _containerName { get ; set ; }

    private CloudStorageAccount _storageAccount { get ; set ; }

 

    private CloudBlobContainer _container;

 

    public AzureBlobFileSystemAgent()

        : this (CST_DEFAULTCONTAINERNAME, CST_DEFAULTACCOUNTSETTING)

    {

    }

 

    public AzureBlobFileSystemAgent(string containerName, string storageAccountConnectionString)

         : this (containerName, CloudStorageAccount.FromConfigurationSetting(storageAccountConnectionString))

    {

    }

 

    public AzureBlobFileSystemAgent(string containerName, CloudStorageAccount storageAccount)

    {

        _containerName = containerName;

         _storageAccount = storageAccount;

 

        // create the blob container for account logos if not exist

        CloudBlobClient blobStorage = _storageAccount.CreateCloudBlobClient();

        _container = blobStorage.GetContainerReference(_containerName);

        _container.CreateIfNotExist();

 

        // configure blob container for public access

        BlobContainerPermissions permissions = _container.GetPermissions();

        permissions.PublicAccess = BlobContainerPublicAccessType.Container;

        _container.SetPermissions(permissions);

    }

 

    #region IFileSystemAgent Members

 

    public void Save(Stream fileStream, string filename, bool overwrite)

    {

        var bytes = new byte [fileStream.Length];

        fileStream.Read(bytes, 0, bytes.Length);

 

        Save(bytes, filename, overwrite);

    }

 

    public void Save(byte [] bytes, string filename, bool overwrite)

    {

        filename = TranslateFileName(filename);

        CloudBlockBlob blob = _container.GetBlockBlobReference(filename);

        if (Exists(filename))

        {

            if (overwrite)

            {

                Delete(filename);

            }

            else

            {

                throw new ApplicationException (string .Format("Existed file {0} please select another name or set the overwrite = true." ));

            }

        }

        blob.UploadByteArray(bytes, new BlobRequestOptions() { Timeout = TimeSpan .FromMinutes(3) });

    }

 

    public byte [] Load(string filename)

    {

        filename = TranslateFileName(filename);

        CloudBlockBlob blob = _container.GetBlockBlobReference(filename);

        return blob.DownloadByteArray();

    }

 

    public bool Exists(string filename)

    {

        filename = TranslateFileName(filename);

        CloudBlockBlob blob = _container.GetBlockBlobReference(filename);

        try

        {

            blob.FetchAttributes();

            return true ;

        }

        catch (StorageClientException ex)

        {

            if (ex.ErrorCode == StorageErrorCode.ResourceNotFound)

            {

                return false ;

            }

            else

            {

                throw ;

            }

        }

    }

 

    public void Delete(string filename)

    {

        filename = TranslateFileName(filename);

        CloudBlockBlob blob = _container.GetBlockBlobReference(filename);

        blob.DeleteIfExists();

    }

 

    private string TranslateFileName(string filename)

    {

        return filename.Replace('/' , '~' ).Replace('//' , '`' );

    }

 

    public string GetResourceUrl(string filename)

    {

        // when using the local storage simulator the blob enpoint without the end '/'

        // but when using the azure it has '/' at the end of it

        // so here i have to use Path.Combine to construct the path and then replace the '/' back to '/'

        var url = Path.Combine(_storageAccount.BlobEndpoint.ToString(), _containerName, TranslateFileName(filename));

        return url.Replace('//' , '/' );

    }

 

     #endregion

}

 

 

在ASP.NET MVC中保存和显示图片

让我以一个 ASP.NET MVC 应用来展示如何使用上述实现。首先我们需要一个辅助类来根据配置初始化相应的IFileSystemAgent  实例。我创建了一个非常简单的工厂类来返回相应的实例(根据在web.config文件中相应的值)。在实际项目中,我们最好使用一些 IoC  容器,例如Unity

  public static class FileSystemAgentFactory

  {

      public static IFileSystemAgent Resolve()

      {

          var config = System.Configuration.ConfigurationManager.AppSettings["filesystem-agent" ];

          switch (config.ToLower())

          {

              case "windows" :

                  if (HttpContext.Current != null && HttpContext.Current.Server != null )

                  {

                      return new WindowsFileSystemAgent(new HttpServerUtilityWrapper(HttpContext.Current.Server), "Upload" );

                }

                else

                {

                    throw new NotSupportedException ("HttpContext ot its Server property is null. The WindowsFileSystemAgent must be used under the web application." );

                  }

              case "blob" :

                   return new AzureBlobFileSystemAgent();

              default :

                  return null ;

          }

      }

  }

 

然后,在处理文件上传的controller中,我们可以使用该工厂类来初始化适当的IFileSystemAgent  实例。如果要保存文件,只需要调用其 Save  方法,而不管实际使用的是哪个实现类。如果我们需要在一般的服务器和Windows Azure之间进行搬移时,我们只需要更改web.config文件。

 

[HttpPost]

  public ActionResult UploadFile(string filekey)

  {

     if (Request.Files != null && Request.Files.Count > 0)

      {

         var file = Request.Files[0];

         var filename = "Avatar/" + Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);

 

         var filesys = FileSystemAgentFactory.Resolve();

         filesys.Save(file.InputStream, filename, true );

 

         Repository.Images.Add(filename);

     }

 

     return RedirectToAction("Index" );

  }

 

 

类似的,当我们在网页上需要显示或者链接文件时,我们也无需关注其具体存储在哪里。为此,我们需要为HtmlHelper 创建一个拓展方法。有了该辅助方法,当我们需要显示或链接文件时,我们只需要使用IFileSystemAgent GetResourceUrl  方法,它便会返回适当的URL。

public static class HelpHelpers

  {

     public static MvcHtmlString Image(this HtmlHelper helper, string filename)

     {

         return Image(helper, FileSystemAgentFactory.Resolve(), filename);

     }

 

     public static MvcHtmlString Image(this HtmlHelper helper, IFileSystemAgent agent, string filename)

     {

         return Image(helper, agent, filename, VirtualPathUtility.GetFileName("/" + filename));

     }

 

     public static MvcHtmlString Image(this HtmlHelper helper, IFileSystemAgent agent, string filename, string

     {

         var html = string .Format("<img src=/"{0}/" alt=/"{1}/" />" , agent.GetResourceUrl(filename), alt);

         return MvcHtmlString.Create(html);

     }

  }

 

 

总结

本文我介绍了如何统一在Windows Azure和一般web应用之间的文件操作代码。相信还可以做进一步的改进和优化。其中之一便是我们可以将HttpServerUtilityBase  以及 CloudStorageAccount  抽离出一个接口来,例如, IRootProvider ,这样会方便进行依赖注入,也可以进行完全的单元测试。 

对于Windows Azure应用,还会有其他的部分可以改进。例如,我们应该将经常会更改的配置数据放入ServiceConfiguration.cscfg ,而不是web.config。这要求我们构建一个 provider 来读取配置信息,我会在后面的文章中进行讲解。 

从 这里 下载本文的展示代码。