最近我全身心的投入到我们第一个基于云的平台-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 接口中,我定义了基本的文件操作方法,例如Save ,Load ,Delete 和Exist s 。GetResourceUrl 方法用于访问文件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 来读取配置信息,我会在后面的文章中进行讲解。
从这里 下载本文的展示代码。