Introduction
Microsoft SharePoint Server 2007 and Windows SharePoint Services 3.0 are excellent tools to help companies organize their content and make it available to those who need it within the business. As companies change and reorganize, this information must be adaptable also. For instance, one division within a company may have started using a meeting site to help organize and collaborate on a project; however, the project may have been taken over by another division with its own site. How can all of the existing information be saved and moved? Although MOSS 2007 and WSS are great platforms, the tools available do fall short in this area. This article will explain options and a possible solution to allow webs to be copied or moved from one location to another.
Background
Let's start by defining a scenario for use in this article. Your company has created a SharePoint site structure similar to this
Root | | | |
Austin | Dallas | Ft. Worth | El Paso |
Divsion1
| Divsion3
| Divsion5
| Divsion7
|
Divsion2
| Divsion4
| Divsion6
| Divsion8
|
This is, of course, very simplistic since in a real-world application, each site would potentially have many lists and subsites; however, this will give us a starting point for discussion. Now, consider when Divsion8 moves from El Paso to Austin. Of course, the site could remain at its present location in the overall site structure, yet, one would logically think to look for it under Austin, not El Paso, since it now resides there. A new subsite could be created under Austin and all of the content manually moved; however, this is a tedious and labor intensive process, and will not preserve audit information or version history.
Warning This project uses SharePoint 2010 Beta and Visual Studio 2010. Although it should work in previous versions, it has not been tested.
Options for copying or moving a site
When needing to copy or move SharePoint sites or webs, the options are really limited.
Central Administrator Backup and Restore
The SharePoint Central Administrator site offers the Backup and Restore function. However, as we can see, it is not very granular. You can back up the entire web application, not a single site or web.
Stsadm
The venerable commandline tool stsadm.exe offers a more granular option for backup.
stsadm -o backup -url http://myserver/Elpaso/Division8
-directory C:\SPBackup -filename division8.bak - backupmethod full
More details about stsadm commands can be found here: http://technet.microsoft.com/en-us/library/cc263441.aspx
There is a limitation on size; backups using stsadm are limited to 15 GB or less, and if using SharePoint prior to SP2, you must also use setsitelock to prevent additions or updates to the site and its content while the backup is in progress. Also, alerts and workflows will not be preserved. To restore the site, you use the mirror command, restore. This is a time consuming two-step process, and can only be performed by someone with administrative rights and access to the stsadm application. Not for the average user.
SharePoint Designer
Using SharePoint Designer 2007, the site that is currently opened can be backed up using the Backup Web Site menu item under the Site -> Administration -> Backup menu.
Note: This option is curiously missing from SharePoint Designer 2010 beta.
To restore the backup to another location, you must first create the site, then restore it. A cumbersome three step process. Further limitations are that you must have access to SharePoint Designer, which not everyone will have.
SPExport/SPImport
Found in the Microsoft.SharePoint.Deployment
namespace, these classes provide a method to basically backup and restore a site, web, list, and other objects in SharePoint, and are what the SharePoint Designer uses. To use SPExport
or SPImport
, you need to configure the process by using the respective settings classes.
SPExportSettings settings = new SPExportSettings();
settings.FileLocation = "C:\SPBackup";
settings.SiteUrl = http:
settings.FileCompression = true;
settings.OverwriteExistingDataFile = true;
settings.BaseFileName = "export";
SPExport export = new SPExport(settings);
export.Run();
This code will create an archive file named export.cmp that contains information about the site collection located at http://server/MySite and place it in the folder C:\SPBackup. Setting FileCompression = false
will produce a compressed folder structure. OverwriteExistingDataFile = true
tells SPExport
to overwrite any existing files. Setting it to false
will produce an exception if the export.cmp file already exists. The reverse operation would be as follows:
SPImportSettings settings = new SPImportSettings();
settings.FileLocation = "C:\SPBackup";
settings.BaseFileName = "export";
SPImport import = new SPImport(settings);
import.Run();
A minor point of interest is when setting CommandLineVerbose = true
in the Settings objects, it will write output to a console window so you can see the extensive operations that are occurring during an export or import. Although there is an event ProgressUpdated
in both the SPExport
and SPImport
classes, it does not send this information. Instead, it only sends information about the total number of objects and how many have been processed.
These are just some basic settings. Now let's take a more detailed look at the process and how it can be used to copy or move a web.
Copy/Move Web
Using the SharePoint API, we can create classes that expose a simple interface for copying or moving a web.
SPWeb web = new SPWeb("http://server/Elpaso/Division8", "http://server/Austin");
web.Copy();
The first parameter is the URL of the web to be copied. The second parameter is the URL of the destination. In this case, we will end up with http://server/Austin/Division8 that is a copy of what is at http://server/Elpaso/Division8.
Validate Source Web
The first thing that must be done is to verify the existence of the source web.
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("Source web is invalid");
}
}
}
catch(System.IO.FileNotFoundException)
{
throw new ArgumentException("SourceURL is invalid");
}
}
An interesting note here is if we tried to call OpenWeb
without passing the relative path of the web we are trying to open. As below, it would return the root web, http://myserver/Home in this case, and of course, SPWeb.Exists
would return true
.
SourceWeb = SourceSite.OpenWeb();
A cumbersome alternative would be to search the SPWebCollection
and attempt to match the name. Using the StringComparer.CurrentCultureIgnoreCase
would be necessary since the names and the URL may not be the same case.
string web = SourceURL.Replace(SourceSite.Url, "").Remove(0, 1);
if(SourceSite.AllWebs.Names.Contains(uri.LocalPath,
StringComparer.CurrentCultureIgnoreCase))
{
SourceWeb = SourceSite.OpenWeb();
}
Validate destination web
The destination web must be validated also. If a web already exists with the same name, we must check its type. It if is not the same as the source, an exception will be generated during the import process, so it must be deleted. It isn't necessary to create a new web as one will be created during the import process. If the webs are the same type, the destination will be overwritten with the source during the import process.
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("Destination web is invalid");
}
}
}
catch(System.IO.FileNotFoundException)
{
throw new ArgumentException("Destination site is invalid");
}
}
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;
}
}
Finding the SPWebTemplate
When comparing the SPWebTemplate
for webs, the obvious first place to look is the WebTemplate
property. However, this does not give enough information for an accurate comparison. For instance, SPWeb.WebTemplate
will return MPS for a web that was created with a Basic Meeting site template and STS for a Blank Site, but the actual template names are MPS#1 and STS#1, respectively. Where does this extra provisioning configuration information come from? Unfortunately, there does not appear to be any information in the SPWeb
object that indicates the provisioning configuration; the only place this is available is in the WSS_Content
database.
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);
}
Here, we get the database connection string from the SPSite
objects and look up the proper value in the Webs table. Of course, it should go without saying that accessing the database directly is not recommended, but, since this information is not available in the API, there is no choice.
Exporting the web
As stated above, the export process is configured using the settings class, SPExportSettings
in this case.
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();
What happens with the above code, however, is the entire site collection will be exported. Basically, a backup of the entire SharePoint site that is the root of the specified SiteURL
. However, we are only interested in a single web. To export a single web, it needs to be added to the ExportedObjects
collection. SPExport
uses this collection to identify what needs to be exported. If this collection is empty, everything will be exported.
SPExportObject expObj = new SPExportObject();
expObj.IncludeDescendants = SPIncludeDescendants.All;
expObj.Id = SourceWeb.ID;
expObj.Type = SPDeploymentObjectType.Web;
settings.ExportObjects.Add(expObj);
We specify the ID of the SourceWeb
and set the type to Web
. The SPDeploymentObjectType
enumeration contains the members to identify: Site
, Web
, List
, ListItem
, File
, and Folder
.
Importing the web
private void Import()
{
if(DestinationSite == null)
throw new ApplicationException("DestinationSite is null");
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();
}
Enhancements and follow-up
Enhancements for future versions could be to provide the option to rename the web while copying or moving, and to update the navigation and quick launch for the parent site.
The code isn't very useful unless it can be used. This article provides background for creating the tool; a follow-up article will show how to expose it for usage.
History
- Initial posting: 4/13/10.