最近我全身心的投入到我们第一个基于云的平台-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 接口中,我定义了基本的文件操作方法,例如SaveLoadDeleteExist 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 来读取配置信息,我会在后面的文章中进行讲解。

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

本文翻译自:http://geekswithblogs.net/shaunxu/archive/2010/08/26/unify-your-file-operation-code-between-azure-and-normal-web.aspx