预测:多云

将内容从 SharePoint 推送到 Windows Azure 存储

Joseph Fultz

下载代码示例

本月我的合著者是我的同事 Shad Phillips,他协助我完成了一个最新项目,即我与我的一个客户一起证明将 SharePoint 2010 用作应用程序平台这一概念。另外,客户的一个职员问我是否能想出一种合理的方式以从 SharePoint 获得批准的内容并发布这些内容,使企业网络外部的人员能使用它们。

客户当前的基础结构不支持外部内容(可下载的文档和视频)。使用 Windows Azure 完成大量工作后,我立即想到将内容推送到 Windows Azure 存储这个操作并入为工作流的一部分应是非常简单的,然后根据需要公开发布内容或提供对受限内容的基于租约的访问。

明确这一点后,我与我的同事 Shad 进行了商谈,他之前在解决类似的问题时实现了将库中的 SharePoint 文档存档到 Windows Azure 存储的示例方法。虽然该解决方案的意图与我的目标不同,但机制是一样的。本月,Shad 和我将逐步演示一个将内容从 SharePoint 推送到 Windows Azure 存储的示例实现,并介绍一些有关针对文件的租约访问控制的内容。

方案和设置

具体而言,我们开发了一个自定义功能,用户可利用此功能有选择地将文档从 SharePoint 推送到 Windows Azure 存储。出于某个无法解释的原因,用户通常在其文档被移动且未提供查找这些文档的链接时不愿执行上述操作,因此我们在文档库中保留了一个指向云位置的链接,文档在该位置的行为与在非云位置的行为相同。

必需的软件:

  • Visual Studio 2010
  • Microsoft SharePoint 2010
  • Windows Azure SDK
  • Windows Azure 开发存储服务

SharePoint 2010 网站和文档库配置

在此方案中,我们使用团队网站模板创建了一个 SharePoint 网站。我们在共享文档库中创建了一个列,该列可用于将项目标记为已存档到 Windows Azure。利用可通过功能区访问的库设置来完成此操作。进入“库设置”后,我们创建一个包含属性的列,如图 1 所示。

图 1 团队网站模板上的列设置

在“高级设置”中,我们还为“允许管理内容类型”设置选择“是”。

我们将其用作已命名为“文档链接”的内容类型的一部分。接下来,我们创建了此内容类型的实例来表示指向已存档文档的链接,如图 2 所示。

图 2 我们的新“文档链接”内容类型

在将列和内容类型添加到文档库后,我们上载了一个名为 Services SOW.docx 的示例 Word 文档。

SharePoint 2010 Web.config

若要连接到云,我们需要获取与 Windows Azure 连接时所需的设置。在此示例中,我们使用了开发存储并将键添加到 web.config 中的 <appSettings> 元素,如图 3 所示。

图 3 在 Web.config 中添加键

SharePoint 项目

幸运的是,对于使用了 Visual Studio 2010 的 SharePoint 2010 来说,创建、调试和发布新功能是绝佳的开发人员体验。我们创建了一个 SharePoint 功能(有关此功能的详细信息,请参阅 msdn.microsoft.com/library/bb861828(office.12)),此功能在文档库的项目操作下拉菜单中添加了一个自定义操作。用户将单击它来使用存档功能。

首先,我们使用空 SharePoint 项目模板创建一个名为 MSSAzureArchive 的解决方案(参见图 4)。

图 4 Visual Studio 2010 中的项目选择

接下来,我们指定了网站和安全级别以进行调试。由于代码需要进行外部调用,而沙盒解决方案不允许这样做,因此我们决定选择“部署为场解决方案”。需要为 Microsoft.Windows.Azure.StorageClient 和 System.Web 的项目添加引用。接下来,我们使用 EmptyElement 模板将一个“空元素”项添加到项目中,并将该项命名为 AzureStorageElement。我们添加了一个 <CustomAction/> 元素,以便将新的操作项添加到文档库项的上下文菜单中(参见图 5)。

图 5 通过“添加新项”来添加 AzureStorageElement

自动将一个名为 Feature1 的新功能添加到项目中,我们已将其重命名为 MSSAzureArchive。我们已将添加的 AzureStorageElement 的 Elements.xml 文件的内容替换为以下内容:

<?xml version="1.0" encoding="utf-8"?>
 <Elements xmlns="https://schemas.microsoft.com/sharepoint/">
   <CustomAction
     Id="UserInterfaceCustomActions.ECBItemToolbar"
     RegistrationType="List"
     RegistrationId="101"
     Location="EditControlBlock"
     Sequence="106"
     Title="Azure Storage">
     <UrlAction Url="~sitecollection/
       _layouts/MSSAzureArchive/
       AzureStorage.aspx?ItemUrl={ItemUrl}" />
   </CustomAction>
 </Elements>

对于缺乏经验的 SharePoint 开发人员,图 6 显示了部分 <CustomAction/> 属性的简要说明(有关 <CustomAction/> 元素及其属性的详细信息,请参阅 msdn.microsoft.com/library/ms460194)。

图 6 <CustomAction/> 元素的属性

属性 功能
ID 唯一标识符。
Location 指定元素应在 SharePoint UI 中出现的位置。在此示例中,项菜单 (EditControlBlock) 是相对于功能区等 UI 的所需位置。
Sequence 指定操作的排序优先级。

请注意 UrlAction 元素的 Url 属性;这是一个为处理存档文档命令而发生的导航操作。SharePoint 根据此配置确定了将功能置于 UI 中的哪个位置,以及在某人单击该功能时应执行的操作。SharePoint 将导航到我们创建的用来处理选定文档的存档的页面。通过此页面,用户可以为项选择一个目标存储容器或创建一个新存储容器,因此我们需要将“应用程序页”项添加到项目。我们再次使用 SharePoint 2010 模板来选择“应用程序页”模板,并将其命名为 AzureStorage.aspx(参见图 7)。

图 7 在 SharePoint 2010 中添加新页

由于此示例并不是为了给任何人留下精美 UI 设计这一印象,因此我们只添加了完成此工作所需的最少控件。在页面标记的 <asp:Content> 元素中,我们添加了代码,如图 8 所示。

图 8 添加所需的最少控件

Document to Archive:
<asp:Label ID="fileName" runat="server" ></asp:Label>   <br/>   
Choose Azure Container:
<asp:DropDownList ID="azureContainers" runat="server"  
  Visible="true"></asp:DropDownList>   
<asp:TextBox id="newContainerName" runat="server" Visible="false"></asp:TextBox>
<asp:Button ID="saveContainer" runat="server" Text="Save Container" 
  OnClick="SaveContainer_Click" Visible="false"></asp:Button>
<br />
<asp:Button ID="createContainer" runat="server" Text="Create New Container" 
  OnClick="CreateContainer_Click" />
<br/>
<asp:Button ID="archiveFile" runat="server" Text="Archive File" 
  OnClick="Archive_Click" />       
<br/>
<asp:Label ID="errMessage" runat="server" Text=""></asp:Label>

接下来,我们编辑了隐藏代码,将 UI 元素连接到一些代码以便与 Windows Azure 存储进行通信,并且呈现了相关信息。我们在页的加载事件中初始化了云存储客户端,并使用以前的 web.config 设置获取了可用容器(参见图 9)。

图 9 初始化云存储客户端

protected void Page_Load(object sender, EventArgs e)
{

  this.InitializeCloudStorage();
  if (!IsPostBack)
  {
    this.GetContainers();
  }
}

private void GetContainers()
{
  IEnumerable<CloudBlobContainer> blobContainers =  
    cloudBlobClient.ListContainers();

  this.azureContainers.DataSource = blobContainers;
  azureContainers.DataTextField = "Name";
  this.azureContainers.DataBind();
  if (azureContainers.Items.Count < 1)
  {
    ListItem defaultContainer = new ListItem(defaultContainerName);
    defaultContainer.Selected = true;
    azureContainers.Items.Add(defaultContainer);
  }
}

由于此处的重点是存档功能,因此我们将集中介绍它。 可通过下载来获取其他代码 (code.msdn.microsoft.com/mag201012Cloudy)。 我们为 archiveFile 按钮添加了单击处理程序并将它与 Archive_Click 函数连接在一起。 我们可根据 UrlAction 元素来检索项的路径。 在该单击函数中,使用对象模型从 SharePoint 提取项,检查它是否已存档 - 如果未存档,则将它上载到选定容器(参见图 10)。

图 10 用于从 SharePoint 提取项的单击函数的代码

protected void Archive_Click(object o, EventArgs e)
{
  try
  {
    webSite = SPContext.Current.Web;
    filePath = webSite.Url.ToString() + 
    Request.QueryString["ItemUrl"].ToString();
    fileToArchive = webSite.GetFile(filePath);
    string sArchived = fileToArchive.Item["IsArchived"].ToString(); 

    bool isArchived = Convert.ToBoolean(sArchived);

    if (isArchived)
    {
      errMessage.Text = "This document has already been archived!";
    }
    else
    {
      string newGuid = Guid.NewGuid().ToString();
      string uniqueBlobName = string.Format(newGuid + "_" + 
        fileToArchive.Name.ToString());

      blobContainer = cloudBlobClient.GetContainerReference(
        this.azureContainers.SelectedValue);
      blobContainer.CreateIfNotExist();
      cloudBlob = blobContainer.GetBlockBlobReference(uniqueBlobName);
      cloudBlob.UploadByteArray(fileToArchive.OpenBinary());

在将项上载到存储后,将创建一个类型为“文档链接”的新存档项来代替原始文档,且原始文档将被删除。 如果这是一个发布示例而非存档示例,则可能不会删除原始项,而是使用已发布版本的链接将其标记为已发布。 原始项用于获取目标文档库和原始文档的路径。 通过添加 IsArchived 属性并分配值“true”将新项标记为已存档。首先,我们执行一些操作来获取部分所需值,然后创建新项并将这些值分配给该项,如下所示:

SPDocumentLibrary docLib = 
  fileToArchive.DocumentLibrary;

Hashtable docProperties = new Hashtable();
docProperties["IsArchived"] = true;
string docLibRelPath = 
  docLib.RootFolder.ServerRelativeUrl;
string docLibPath = string.Empty;
webSiteCollection = SPContext.Current.Site;
docLibPath = 
  webSiteCollection.MakeFullUrl(docLibRelPath);

string azureURL = cloudBlob.Uri.ToString();

函数 BuildLinkToItem 使用 Windows Azure 存储中项的路径创建了内容类型“文档链接”的实例。 该内容类型实例将添加到库中,作为通过 SharePoint UI 从 Windows Azure 存储检索项的链接,如下所示:

string azureStub = this.BuildLinkToItem(azureURL).ToString();

  SPFile newFile = webSite.Files.Add(documentPath,     
    UTF8Encoding.UTF8.GetBytes(azureStub), docProperties, true);

  SPListItem item = newFile.Item;
  item["Content Type"] = "Link to a Document";
  SPFieldUrlValue itemUrl = new SPFieldUrlValue();
  itemUrl.Description = fileToArchive.Name;
  itemUrl.Url = azureURL;
  item["URL"] = itemUrl;
  item["IsArchived"] = true;
  item.Update();
  fileToArchive.Delete();

使用完成的代码保存文档,将其移动并替换为指向 Windows Azure 存储的链接后,就该侧重于解决方案的生成和部署了。我们双击 Package.package 文件以显示包设计器,然后选择屏幕底部的“高级”选项卡。我们将所需的包程序集添加到此处,以便包含 Microsoft.WindowsAzure.StorageClient.dll。为了简化该示例,我们将部署目标设置为 GlobalAssemblyCache。我们通过以下操作来确保开发存储处于运行状态:导航到服务器资源管理器,单击“Windows Azure 存储”节点,然后单击“(开发)”节点。

顾不了这么多了,我们按 F5 键来构建、部署、附加到进程并启动浏览器会话以开始调试功能。我们导航回到之前提及的共享文档库,并打开附加到之前加载的文档的下拉菜单。在下拉菜单中,我们选择了新元素“Azure 存储”,我们可通过它进入自定义应用程序页以选择目标容器(参见图 11)。

图 11 选择 Windows Azure 存储元素

虽然我们之前已在此页上创建了新容器,但我们将改用已创建的文档容器并单击“存档文件”按钮来执行前面的代码(参见图 12)。

图 12 选择容器

在将文件存档到 Windows Azure 存储后,我们便导航回到共享文档库。不会显示文档,而是显示替换 Services SOW.docx Word 文档的“文档链接”项(参见图 13)。

图 13 SharePoint 文档库中的“文档链接”

当我们查看项的属性时,会看到与内容类型相关的字段,特别是指向文档在 Windows Azure 存储中当前位置的 URL(参见图 14)。

图 14 链接属性

我们可以通过单击“文档链接”直接从 Windows Azure 存储打开文档。可以使用 URL 属性直接访问它或通过其他代码或 UI 访问它。例如,如果我们仍需要通过 SharePoint 索引服务为这些项建立索引,则可以创建一个知道如何处理“文档链接”内容类型以确保为内容建立正确索引的自定义 IFilter。

实现将 SharePoint 文档库中的内容存档到 Windows Azure 存储容器已不成问题,这样,对于未经身份验证的请求,我们现在只能公开访问甚至无法访问已存档文档。

发布时的访问控制

我之前说过,促使我与 Shad 谈论其存档部分的原因是使用 Windows Azure 存储作为一种提供用于访问已经过审阅和批准的内容的公众登录点的方式。在我考虑的示例中,没有必要包含任何访问控制,因为文档将与每个人共享。但仍留出了几分钟时间供用户提问题:“如果我们需要发布一些内容且只允许某些人(如供应商、客户或员工)访问这些内容,该怎么办?”类似这样的任务通常在公司内部采用以下方式完成:将相关人员加入公司域,或以某种方式将他们联合起来以便通过用户名和密码问题对其进行标识。此处的示例不是这种情况,客户并不是真的想设置一些应用程序层或前端来控制访问;开发前端会增加实现成本,从而降低价值。

一种解决方案是在 Blob 上使用 SharedAccessPolicy。容器和容器中的 Blob 会使用少量代码将其 PublicAccess 设置为“关闭”,您在执行 Windows Azure 存储开发时可能要编写这些代码。以下代码示例演示如何将 PublicAccess 设置为“关闭”,但考虑到容器上的 SharedAccess,我应生成并分发签名:

BlobContainerPermissions permissions = new BlobContainerPermissions();
  permissions.PublicAccess = BlobContainerPublicAccessType.Off;

  SharedAccessPolicy accesspolicy = new SharedAccessPolicy();
  accesspolicy.Permissions = SharedAccessPermissions.Read;
  permissions.SharedAccessPolicies.Add("Read", accesspolicy);

  BlobContainer.SetPermissions(permissions);

如果我们直接请求存储容器中的资源,则会收到消息“未找到页面 404”。在我们上载 Blob 时,我们对 Blob 自身执行了少量类似工作,但我们创建了一个允许读取的 SharedAccessPolicy,为它设置了一个过期时间,并向后请求“共享访问签名”,与以下内容类似:

SharedAccessPolicy policy = new SharedAccessPolicy();
policy.Permissions = SharedAccessPermissions.Read;
policy.SharedAccessExpiryTime = DateTime.Now.AddDays(5);
string SharedAccessSignature = destBlob.GetSharedAccessSignature(policy));

调用 GetSharedAccessSignature 将返回一个与以下内容类似的字符串:

?se=2010-08-26T18%3A22%3A07Z&sr=b&sp=r&sig=WdUHKvQYnbOcMwUdFavn4QS0lvhAnqBAnVnC6x0zPj8%3D

如果我将查询字符串连接到 Blob 的 URI 的结尾处,则应将它收回,前提是未过期。有关签名和共享访问策略的详细信息,请参阅 msdn.microsoft.com/library/ee395415

为了解决此问题,我将生成签名并提供到期日期较长的已签名 URI,这样便能在上载时轻松创建它们,然后存储指向已发布文档的链接的列表。为了提供更好的安全性并为单个用户提供短时间的访问权,我需要一个用户界面。通过此用户界面,用户可以请求对一个或多个资源的访问权,并获得将提供短期访问权的已签名 URI。

与云混合

Shad 和我在这里使用了一个常规实现来讨论两个不同的方案。这对两个方案都特别有用,因为我们需要云提供的特殊功能(可伸缩的、可靠的且可扩展的存储),无需做太多的设置工作,而且只需为使用的内容付费。我们想表达的主要意思是,当专业人员希望为我们的客户(内部或外部)创建解决方案时,我们的解决方案概念不必单独包含在云中或内部部署中。可轻松地将两者混合在一起。随着服务更新的应用,将公司网络和云网络混合在一起会变得越来越简单。我希望以后的混合能够达到二者大同小异的程度。当您查看软件或业务系统的解决方案时,可以暂停下来花时间思考一下“云中是否有可帮助我的内容?”

Joseph Fultz 是达拉斯 Microsoft 技术中心的架构师,协助企业客户和 ISV 设计和制作软件解决方案以满足商业和市场需求。他在 Tech·Ed 及类似的内部培训活动中做过讲座。

Shad Phillips* 是达拉斯 Microsoft 技术中心的架构师,协助企业客户和合作伙伴设计和部署基于 Microsoft SharePoint 2010 构建的企业内容管理解决方案。 *

衷心感谢以下技术专家对本文的审阅:Jasen Tenney