您的位置:首页 > 编程语言

统一Windows Azure和一般web应用之间的文件操作代码(转+译)

2010-12-01 10:59 288 查看
[b]公告
:本博客为微软云计算中文博客
的镜像博客。

部分文章因为博客兼容性问题
,会影响阅读体验
。如遇此情况,请访问
原博客

[/b]



最近我全身心的投入到我们第一个基于云的平台-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 来读取配置信息,我会在后面的文章中进行讲解。

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

本文翻译自:http://geekswithblogs.net/shaunxu/archive/2010/08/26/unify-your-file-operation-code-between-azure-and-normal-web.aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐