介绍
SharePoint是一个很优秀的工具,可以帮助企业组织内部的信息,将信息提供给需要的人。当公司内部组织发生变化或重组后,这些信息也必须作相应的调整以适应新的组织结构。例如,公司的某个分支机构原先已经使用一个会议站点来组织一个项目的内容,并且方便干系人的协作。但是,该项目可能会被另一个分支机构接手,需要转移到该分支机构自己的站点下。如何才能将现有的信息进行保存和移动?虽然SharePoint是一个很伟大的平台,但是这方面的工具很短缺。这里,我们将提供一种选择或者说一个可能的解决方案,以允许网站从一个位置复制或移动到另一个地方。
背景
让我们先为本文中的方案定义一个场景。假设你的公司创建了一个SharePoint网站,其结构类似下表所示:
根站点 | |||
北京 | 天津 | 上海 | 深圳 |
分支机构1 | 分支机构3 | 分支机构5 | 分支机构7 |
分支机构2 | 分支机构4 | 分支机构6 | 分支机构8 |
这当然是一个非常简单的真实世界的应用,因为每个站点下可能会有很多列表和子网站。但是,这将提供给我们一个讨论的起点。现在,假设分支机构8从深圳搬到了北京。当然,如果为了保持整个网站的结构,该网站还可以放在目前的位置上,但是从逻辑上来说,人们会从北京的网站下查找该分支机构的网站,因为现在的归属如此。所以,我们可能会在北京下面新建一个子网站,然后手动移动所有内容。然而,这是一项繁琐的、劳动密集的过程,而且不会保留审计信息或版本历史。
注意:此项目使用SharePoint 2010和Visual Studio 2010。尽管应该在以前的版本中也可以使用,但尚未经过测试。
复制或移动一个站点的工具选择
当需要复制或移动一个SharePoint网站集或网站时,可以选择的工具十分有限。
管理中心里的备份和还原
管理中心提供了SharePoint网站的备份和恢复功能。在SharePoint 2007里,该备份还不是很精细。这里可以执行对整个Web应用程序的备份。
对于单独的一个网站集或网站或列表,SharePoint 2010提供了粒度备份。
导出网站和列表的界面:
还有一个很有用的功能,从未连接的内容数据库中提取信息:
Stsadm
古老的命令行工具Stsadm.exe,也是原先备份还原时用得最多的工具。在SharePoint 2010中仍然提供了备份功能。参数很细致。
-directory C: \ SPBackup -filename division8 . bak - backupmethod full
关于stsadm参数的详细设置可以参考TechNet上的这篇文章。
使用Stsadm有一个大小限制:内容不超过15 GB。如果是SharePoint 2007 SP2之前的版本,还必须用setsitelock项来防止在备份过程中添加或修改网站的内容。此外,订阅和工作流将不会被保留。若要恢复网站,使用其镜像命令:restore。这两个步骤都是很耗时,并且只能由具有管理员权限的人来执行。 并不是为普通用户准备的。
SharePoint Designer
使用SharePoint Designer 2007打开SharePoint 2007站点时,当前打开的网站可以通过“网站”->“管理”->“备份网站”菜单进行备份。很奇怪,在SharePoint Designer 2010里找不到此项。
要恢复备份到另一个位置,则必须先创建该网站,然后再进行还原。有些累赘的三个步骤。同时用这种方式的限制是,你必须能够访问SharePoint Designer,并不是每个人都装SharePoint Designer。
SPExport/SPImport
在Microsoft.SharePoint.Deployment命名空间下,我们找到SPExport和SPImport类,其中包含了最基本的备份和还原一个网站集,网站,列表或SharePoint中的其他对象的方法,这些方法也正是SharePoint Designer所使用的。为了能够使用SPExport和SPImport,我们首先需要通过相应的设置类配置整个过程中涉及的细节信息。
SPExportSettings settings = new SPExportSettings();
settings.FileLocation = "C:\SPBackup";
settings.SiteUrl = http://server/mySite
settings.FileCompression = true;
settings.OverwriteExistingDataFile = true;
settings.BaseFileName = "export";
SPExport export = new SPExport(settings);
export.Run();
此代码将创建一个存档文件,名为export.cmp,其中包含了网站集http://server/MySite中的所有信息,最后存放在文件夹C:\SPBackup下。设置FileCompression = true会产生一个CAB压缩包。 OverwriteExistingDataFile = true告诉SPExport覆盖任何现有文件。如果将其设置为false,并且export.cmp文件已存在,则将抛出一个异常。反方向的操作如下:
SPImportSettings settings = new SPImportSettings();
settings.FileLocation = "C:\SPBackup";
settings.BaseFileName = "export";
SPImport import = new SPImport(settings);
import.Run();
其中比较有趣的一点是,当在Settings对象中设置CommandLineVerbose = true时,会将输出信息写入到一个控制台窗口中,我们就可以方便的看到导入导出期间经历的详细过程。虽然SPExport和SPImport类都有一个ProgressUpdated事件,但是它不会发送此类信息。它只会发送相关对象的总数和已被处理的数量。
这仅仅是一些基本的设置。现在让我们更详细的研究一下这一过程,看看如何用它来复制或移动一个网站。
复制/移动网站
我们可以创建一个类,为复制或移动网站提供一个简单的接口。
SPWeb web = new SPWeb("http://server/shenzhen/Division8", "http://server/beijing");
web.Copy();
第一个参数是被复制网站的网址。第二个参数是目标URL。在本例中,我们是将http://server/shenzhen/Division8复制到http://server/beijin/Division8。
验证源网站
第一件必须做的事情是验证源网站的存在。
private void ValidateSource()
{
try
{
Uri uri = new Uri(SourceURL);
SourceSite = new Microsoft.SharePoint.SPSite(SourceURL);
if(SourceSite != null)
{
SourceWeb = SourceSite.OpenWeb(uri.LocalPath);
if(!SourceWeb.Exists)
{
SourceWeb.Dispose();
SourceWeb = null;
throw new ArgumentException("源网站无效");
}
}
}
catch(System.IO.FileNotFoundException)
{
// 找不到指定的网站
throw new ArgumentException("SourceURL is invalid");
}
}
这里一个有趣的地方是,如果我们调用OpenWeb时没有传递想要打开的网站的相对路径,像下面这样,则会返回根网站,当然,SPWeb.Exists将返回true。
SourceWeb = SourceSite.OpenWeb();
一个繁琐但可行的方法是搜索SPWebCollection以匹配名称。使用StringComparer.CurrentCultureIgnoreCase很必要,因为名字和URL不见得大小写相同。注:这样的方法对中文标题的网站无效,因为通常中文的标题和url名称往往不同,相同的内容也要decode后才能比较,不是简单的大小写的问题了。
string web = SourceURL.Replace(SourceSite.Url, "").Remove(0, 1);
if(SourceSite.AllWebs.Names.Contains(uri.LocalPath,
StringComparer.CurrentCultureIgnoreCase))
{
SourceWeb = SourceSite.OpenWeb();
}
验证目标网站
目标网站同样需要验证。如果同名的网站已经存在,我们必须检查器类型。如果和源网站不同,在导入过程中会抛出一个异常。因此必须首先将其删除。不必提前创建好一个新网站,因为在导入过程中会自动创建。如果网站的类型相同,在导入过程中目标网站会被覆盖。
private void ValidateDestination()
{
try
{
Uri uri = new Uri(DestinationURL);
DestinationSite = new Microsoft.SharePoint.SPSite(DestinationURL);
if(DestinationSite != null)
{
DestinationWeb = DestinationSite.OpenWeb(uri.LocalPath);
if(DestinationWeb.Exists)
{
CompareTemplates();
}
else
{
throw new ArgumentException("目标网站无效");
}
}
}
catch(System.IO.FileNotFoundException)
{
// 找不到指定网站
throw new ArgumentException("目标网站无效");
}
}
private void CompareTemplates()
{
uint localID = Convert.ToUInt16(SourceWeb.Locale.LCID);
string templateName =
GetTemplateName(SourceSite.ContentDatabase.DatabaseConnectionString,
SourceWeb.ID, SourceWeb.WebTemplate);
SPWebTemplate sourceTemplate =
SourceSite.GetWebTemplates(localID)[templateName];
templateName =
GetTemplateName(DestinationSite.ContentDatabase.DatabaseConnectionString,
DestinationWeb.ID, DestinationWeb.WebTemplate);
SPWebTemplate destTemplate =
DestinationSite.GetWebTemplates(localID)[templateName];
//如果模板不同,则需要先删除目标网站
if(sourceTemplate.Name != destTemplate.Name)
{
RecursivelyDeleteWeb(DestinationWeb);
DestinationWeb.Dispose();
DestinationWeb = null;
}
}
查看SPWebTemplate
当对网站的SPWebTemplate进行比较时,着眼点首先会放在WebTemplate属性上。然而,该属性并不能为准确的比较提供足够的信息。比如,对于使用基本会议工作区模板创建的一个网站来说,SPWeb.WebTemplate会返回MPS。对于使用空白网站模板创建的一个网站,会返回STS。而准确的模板名称分别应该是MPS#1和STS#1。这些特定的配置信息在哪儿呢?很不幸,SPWeb对象上貌似找不到。唯一可以找到该信息的地方是WSS_Content内容数据库。
private string GetTemplateName(string connString, Guid id, string webTemplate)
{
string cmdText = string.Format("SELECT ProvisionConfig FROM " +
"dbo.Webs WHERE Id = '{0}'", id.ToString());
int provisionConfig = 0;
using(SqlConnection conn = new SqlConnection(connString))
{
using(SqlCommand cmd = new SqlCommand(cmdText, conn))
{
conn.Open();
provisionConfig = Convert.ToInt32(cmd.ExecuteScalar());
}
}
return string.Format("{0}#{1}", webTemplate, provisionConfig);
}
这里,我们通过SPSite对象获取连接字符串,然后再Webs表中查找相应的值。当然,仍然要提醒一句,直接访问数据库是不推荐的。但是,像我们这种情况,在API中实在找不到,因此没得选。
导出网站
正如上面提到的,导出过程的配置需要一个settings类,这里是SPExportSettings。
SPExportSettings settings = new SPExportSettings();
settings.FileLocation = ExportPath;
settings.BaseFileName = EXPORT_FILENAME;
settings.SiteUrl = SourceSite.Url;
settings.ExportMethod = SPExportMethodType.ExportAll;
settings.FileCompression = true;
settings.IncludeVersions = SPIncludeVersions.All;
settings.IncludeSecurity = SPIncludeSecurity.All;
settings.ExcludeDependencies = false;
settings.ExportFrontEndFileStreams = true;
settings.OverwriteExistingDataFile = true;
SPExport export = new SPExport(settings);
export.Run();
运行上面的代码会发生什么呢?是不是整个网站集都被导出了?整个SharePoint网站集是指通过SiteURL指定的站点的根网站及其下的所有子站点。然而,我们只是要其中的一个。若要导出一个网站,需要将其添加到ExportedObjects集合中。SPExport会使用该集合来标识哪些内容需要导出。如果该集合为空,就会导出所有内容。
SPExportObject expObj = new SPExportObject();
expObj.IncludeDescendants = SPIncludeDescendants.All;
expObj.Id = SourceWeb.ID;
expObj.Type = SPDeploymentObjectType.Web;
settings.ExportObjects.Add(expObj);
我们指定了SourceWeb的ID,同时将Type设置为Web。在SPDeploymentObjectType枚举中包括了一系列的成员分别用于标识:Site,Web,List,ListItem,File和Folder。
导入网站
private void Import()
{
if(DestinationSite == null)
throw new ApplicationException("目标网站为空");
SPImportSettings settings = new SPImportSettings();
settings.FileLocation = ExportPath;
settings.BaseFileName = EXPORT_FILENAME;
settings.IncludeSecurity = SPIncludeSecurity.All;
settings.UpdateVersions = SPUpdateVersions.Overwrite;
settings.RetainObjectIdentity = false;
settings.SiteUrl = DestinationSite.Url;
settings.WebUrl = DestinationURL;
SPImport import = new SPImport(settings);
import.Run();
}
后续增强
现在我们的代码还没有什么实际用处。除非在其他代码中被调用。这里只是为接下来的工作做了一个背景铺垫。接下来的博文中,你会看到如何在不同的SharePoint开发方式中调用该类的方法实现复制或移动网站功能。
参考资料