最近我全身心的投入到我们第一个基于云的平台-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 来读取配置信息,我会在后面的文章中进行讲解。
从这里 下载本文的展示代码。
相关推荐
Windows Azure
全书共12章,内容包括云计算概论、云计算技术概观、初探Windows Azure、Windows Azure应用程序开发基础、Windows Azure应用程序开发:Table存储服务、Windows Azure应用程序开发:BLOB存储服务、Windows Azure应用...
这个是将ASP.NETWeb应用程序部署到WindowsAzure网站的一个实例,这是本人参加微软训练营的一个课件,希望对各位有用。
Windows Azure从入门到精通含源代码,接受微软的云服务平台Windows Azure帮助包含源代码
Windows Azure 概述
微软云计算平台技术详解》来自于微软 windows azure mvp 的技术实践和心得体会,主要包括windows azure 平台的基础知识、使用方法、功能特点以及如何基于windows azure 平台设计高可靠、可扩展的应用程序,几乎涵盖...
Windows+Azure实战 资源是从华章出版社官网下载的
Azure是一种灵活和支持互操作的平台,它可以被用来创建云中运行的应用或者通过基于云的特性来加强现有应用。它开放式的架构给开发者提供了Web应用、互联设备的应用、个人电脑、服务器、或者提供最优在线复杂解决方案...
《Windows Azure从入门到精通》针对微软的云计算平台windows azure,循序渐进地介绍了如何构建和管理云端的可扩展应用,一次一个知识点,同时辅之以适当的练习,可帮助读者轻松掌握基本的编程技能,掌握windows ...
《实战Windows Azure:微软云计算平台技术详解》来自于微软 Windows Azure MVP 的技术实践和心得体会,主要包括Windows Azure 平台的基础知识、使用方法、功能特点以及如何基于Windows Azure 平台设计高可靠、可扩展...
Windows Azure云平台基本操作手册.docx
题目:关于azure web 应用4分钟空闲连接的限制Azure web 后台在处理耗时较长的请求时,并且在此期间,客户端和azure web服务没有数据交互,
Windows Azure 的主要目标是为开发者提供一个平台,帮助开发可运行在云服务器、数据中心、Web 和PC 上的应用程序。云计算的开发者能使用微软全球数据中心的储存、计算能力和网络基础服务。Azure 服务平台包括了以下...
windows azure
微软WindowsAzure云应用开发实践整理.pdf
简要描述Windows Azure最新的功能、特色和主要的服务
Windows Azure入门教学系列
Build, deploy and manage cloud solutions using combination of Windows Azure Pack, System Center and Hyper-V Impress your peers at work by learning to build applications that can leverage the cloud to ...
Windows Azure Windows Azure Windows Azure Windows Azure Windows Azure Windows Azure Windows Azure Windows Azure Windows Azure 公有云平台是 微软 云操作系统愿景的三大重要组成部分之一,这平台将会转变 ...
微软文库:使用 ASP.NET Core 和 Azure 构建新式 Web 应用程序-118