在Azure Web站点上使用AzureBlob存储服务来存储图片是一个常见的需求。本文将介绍如何设置Azure存储账户,安装Azure SDK,并在ASP.NET MVC 4网站上实现图片上传到Azure Blob存储的完整流程。
在Azure门户中创建一个存储账户非常简单。需要提供一个账户名称。每个存储账户可以有多个“容器”,这样就可以在多个站点之间共享同一个存储账户。
使用Web Platform Installer安装AzureSDK。为VS 2012安装了1.8版本。
对于Azure web角色项目,可以配置Visual Studio自动启动Azure存储模拟器,但对于常规的ASP.NET MVC项目,这个选项可能不可用。模拟器是csrun.exe,它需要使用/devstore命令行参数来启动存储模拟器:
C:\Program Files\Microsoft SDKs\Windows Azure\Emulator\csrun.exe /devstore
为了方便,将这个命令添加到了Visual Studio的外部工具列表中,这样就可以快速启动它。启动后,系统托盘中会出现一个新的图标,让可以访问UI,它显示了它正在运行的端口。
在开发过程中,希望使用模拟器,这需要一个连接字符串。对于ASP.NET MVC站点,需要直接进入web.config文件并输入一个新的连接字符串。模拟器的连接字符串非常简单:
<connectionStrings>
<add name="StorageConnection" connectionString="UseDevelopmentStorage=true" />
</connectionStrings>
对于大多数Web开发者来说,这可能是非常基础的内容,但花了一些时间才找到一个好教程。以下是如何使用Razor语法制作一个基本表单,让用户选择并上传文件:
@using (Html.BeginForm("ImageUpload", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div>
请选择要上传的图片
</div>
<input name="image" type="file">
<input type="submit" value="上传图片" />
}
现在在AdminController的ImageUpload方法中,可以使用Request.Files访问器来访问上传的文件的详细信息,它返回HttpPostedFileBase的一个实例:
[HttpPost]
public ActionResult ImageUpload()
{
string path = @"D:\Temp\";
var image = Request.Files["image"];
if (image == null)
{
ViewBag.UploadMessage = "Failed to upload image";
}
else
{
ViewBag.UploadMessage = String.Format("Got image {0} of type {1} and size {2}", image.FileName, image.ContentType, image.ContentLength);
// TODO: actually save the image to Azure blob storage
}
return View();
}
现在需要向项目添加对Microsoft.WindowsAzure.StorageClient的引用,这将使能够访问Microsoft.WindowsAzure和Microsoft.WindowsAzure.StorageClient命名空间。
大多数教程会告诉通过简单地传入连接字符串的名称来连接到存储账户:
var storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnection");
但由于使用的是Azure网站而不是Web角色,这会引发异常("SetConfigurationSettingPublisher needs to be called before FromConfigurationSetting can be used")。有几种方法可以修复这个问题,但认为最简单的是直接调用Parse并传入web.config中的连接字符串:
var storageAccount = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnection"].ConnectionString);
存储账户可以有多个“容器”,所以需要提供一个容器名称。对于这个示例,将称之为“productimages”并给它公开访问权限。
blobStorage = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobStorage.GetContainerReference("productimages");
if (container.CreateIfNotExist())
{
// configure container for public access
var permissions = container.GetPermissions();
permissions.PublicAccess = BlobContainerPublicAccessType.Container;
container.SetPermissions(permissions);
}
选择的容器名称实际上必须是有效的DSN名称,否则会得到一个奇怪的“一个请求输入超出范围”的错误。这意味着不能使用大写字母,所以"ProductImages"将不起作用。
最后,准备实际保存图片。需要给它一个唯一的名称,将使用GUID,后面跟着原始扩展名,但可以使用任何喜欢的命名策略。在BLOB名称中包含容器名称可以节省额外调用blobStorage.GetContainer的调用。
string uniqueBlobName = string.Format("productimages/image_{0}{1}", Guid.NewGuid().ToString(), Path.GetExtension(image.FileName));
CloudBlockBlob blob = blobStorage.GetBlockBlobReference(uniqueBlobName);
blob.Properties.ContentType = image.ContentType;
blob.UploadFromStream(image.InputStream);
注意:必须做出的一个稍微令人困惑的选择是创建一个块BLOB还是一个页面BLOB。页面BLOB似乎针对的是需要随机访问读取或写入的BLOB(例如视频文件),不需要为服务图片,所以块BLOB似乎是最佳选择。
现在图片在BLOB存储中,但它在哪里?可以在创建它之后通过调用blob.Uri来找出:
blob.Uri.ToString();
在Azure存储模拟器环境中,这将返回类似于以下内容:
http://127.0.0.1:10000/devstoreaccount1/productimages/image_ab16e2d7-5cec-40c9-8683-e3b9650776b3.jpg
如何跟踪放入容器的内容?在Visual Studio中,在“服务器资源管理器”工具窗口中,应该有一个WindowsAzureStorage节点,它让可以看到模拟器上的容器和BLOB。也可以从那里删除BLOB,如果不想在代码中这样做。
Azure门户具有类似的功能,允许管理BLOB容器,查看它们的内容,并删除BLOB。
如果想要从代码中查询容器中的所有BLOB,只需要以下内容:
var imagesContainer = blobStorage.GetContainerReference("productimages");
var blobs = imagesContainer.ListBlobs();
到目前为止,已经对存储模拟器做了所有事情。现在需要实际连接到Azure存储。为此,需要一个实际的连接字符串,看起来像这样:
DefaultEndpointsProtocol=https;AccountName=YourAccountName;AccountKey=YourAccountKey
账户名称是在第一步中输入的,当创建Azure存储账户时。账户密钥可以在Azure门户中通过点击底部的“管理密钥”链接获得。如果想知道为什么有两个密钥,以及应该使用哪一个,它仅仅是为了让能够在不停机的情况下更改密钥,所以可以使用任何一个。
为了确保实时站点正在运行Azure存储账户,需要创建一个web.config转换,因为Web部署向导似乎不知道Azure存储账户,所以不能像SQL连接字符串那样自动提供这个功能。
这是在Web.Release.config中的转换:
<connectionStrings>
<add name="StorageConnection" connectionString="DefaultEndpointsProtocol=https;AccountName=YourAccountName;AccountKey=YourAccountKey" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
</connectionStrings>