As part of my upgrade I’ve got several hundred web sites that need to be moved around (either from one web application to another or just to a different location with the same web app or site collection). I figured that I needed an easy way to do this – the usual way is to either use the browser and go to the content and structure page to move webs around within a site collection or use the stsadm import and export commands to move between site collections.

What I wanted was a single command that I could call to move a web around regardless of what my target was (the same site collection or a different one). To move a web within the same site collection is really easy – you just set the ServerRelativeUrl property of the SPWeb object to the new path and then call Update() on the web object:

 1using (SPSite sourceSite = new SPSite(url))
 2using (SPWeb sourceWeb = sourceSite.OpenWeb())
 3using (SPSite parentSite = new SPSite(parentUrl))
 4using (SPWeb parentWeb = parentSite.OpenWeb())
 5{
 6    ...
 7    if (sourceSite.ID == parentSite.ID)
 8    {
 9        sourceWeb.ServerRelativeUrl = ConcatServerRelativeUrls(parentWeb.ServerRelativeUrl, sourceWeb.Name);
10        sourceWeb.Update();
11    }
12    else
13    {
14        ...
15    }
16}

Moving a web to a different site collection requires a bit more effort. This is where you end up having to export the source web, create a placeholder web at the target, and then import the exported site to the placeholder site. Fortunately I already had some code to handle calling out to the stsadm import and export commands so I didn’t have re-invent that (it’s much easier to use these built in commands than to try and recreate them via the content deployment API – see the gl-importlist, gl-exportlist, and gl-copylist commands that I created earlier for examples of what’s involved). Thanks to those existing helper methods the code became fairly simple and quick to write:

 1/// <summary>
 2/// Runs the specified command.
 3/// </summary>
 4/// <param name="command">The command.</param>
 5/// <param name="keyValues">The key values.</param>
 6/// <param name="output">The output.</param>
 7/// <returns></returns>
 8public override int Run(string command, StringDictionary keyValues, out string output)
 9{
10    output = string.Empty;
11
12    InitParameters(keyValues);
13
14    string url = Params["url"].Value.TrimEnd('/');
15    string parentUrl = Params["parenturl"].Value.TrimEnd('/');
16
17    using (SPSite sourceSite = new SPSite(url))
18    using (SPWeb sourceWeb = sourceSite.OpenWeb())
19    using (SPSite parentSite = new SPSite(parentUrl))
20    using (SPWeb parentWeb = parentSite.OpenWeb())
21    {
22        if (sourceWeb.ID == parentWeb.ID)
23        {
24            throw new Exception("Source web and parent web cannot be the same.");
25        }
26        if (sourceWeb.ParentWeb.ID == parentWeb.ID)
27        {
28            throw new Exception(
29                "Parent web specified matches the source web's current parent - move is not necessary.");
30        }
31
32        if (sourceSite.ID == parentSite.ID)
33        {
34            // This ones the easy one - just need to set the property and update the web.
35            sourceWeb.ServerRelativeUrl = ConcatServerRelativeUrls(parentWeb.ServerRelativeUrl, sourceWeb.Name);
36            sourceWeb.Update();
37        }
38        else
39        {
40            // Now for the hard one - we need to move to another site collection which requires using the export/import commands.
41            bool haltOnWarning = Params["haltonwarning"].UserTypedIn;
42            bool haltOnFatalError = Params["haltonfatalerror"].UserTypedIn;
43            bool includeUserSecurity = Params["includeusersecurity"].UserTypedIn;
44
45            string fileName =
46            ConvertSubSiteToSiteCollection.ExportSite(url, haltOnWarning, haltOnFatalError, true, includeUserSecurity, true);
47
48            string newWebUrl = ConcatServerRelativeUrls(parentUrl, sourceWeb.Name);
49            CreateWeb(newWebUrl);
50
51            ConvertSubSiteToSiteCollection.ImportSite(fileName, newWebUrl, haltOnWarning, haltOnFatalError, true, includeUserSecurity, true);
52
53            DeleteSubWebs(sourceWeb.Webs);
54
55            sourceWeb.Delete();
56
57            Directory.Delete(fileName, true);
58        }
59    }
60
61    return 1;
62}
63 
64#endregion
65 
66/// <summary>
67/// Deletes the sub webs.
68/// </summary>
69/// <param name="webs">The webs.</param>
70private static void DeleteSubWebs(SPWebCollection webs)
71{
72    foreach (SPWeb web in webs)
73    {
74        if (web.Webs.Count > 0)
75            DeleteSubWebs(web.Webs);
76
77        web.Delete();
78    }
79}
80 
81/// <summary>
82/// Creates the web.
83/// </summary>
84/// <param name="strFullUrl">The STR full URL.</param>
85private static void CreateWeb(string strFullUrl)
86{
87    string strWebTemplate = null;
88    string strTitle = null;
89    string strDescription = null;
90    uint nLCID = 0;
91    bool convert = false;
92    bool useUniquePermissions = false;
93
94    using (SPSiteAdministration administration = new SPSiteAdministration(strFullUrl))
95        administration.AddWeb(Utilities.GetServerRelUrlFromFullUrl(strFullUrl), strTitle, strDescription, nLCID, strWebTemplate, useUniquePermissions, convert);
96}

The command I created, gl-moveweb, is detailed below.

Using this command is pretty straightforward – you just pass in the url of the source web and the url of the new parent web. I’ve got a couple of checks to make sure that you’re not trying to set the parent to itself. If you’re moving the web to a new site collection then you have 3 optional parameters of which only the -includeusersecurity has any real affect (the others just stop either the import or export if there’s a warning or fatal error depending on which option you provide). The syntax of the command can be seen below:

C:\>stsadm -help gl-moveweb

stsadm -o gl-moveweb

Moves a web.

Parameters:
        -url <url of web to move>
        -parenturl <url of parent web>
        [-haltonwarning (only considered if moving to a new site collection)]
        [-haltonfatalerror (only considered if moving to a new site collection)]
        [-includeusersecurity (only considered if moving to a new site collection)]
        [-retainobjectidentity (only considered if moving to a new site collection)]

Here’s an example of how to move a web within the same site collection:

stsadm –o gl-moveweb -url "http://intranet/topics/divisions" -parenturl "http://intranet"

Here’s an example of how to move a web to a different site collection:

stsadm –o gl-moveweb -url "http://intranet/sites/projectATeamSite" -parenturl "http://teamsites/projects" -includeusersecurity -haltonfatalerror

Update 9/21/2007: I’ve enhanced the command to take advantage of another new command I created: gl-updatev2tov3upgradeareaurlmappings (updates the url mapping of V2 bucket webs to V3 webs thereby reflecting the change of url as a result of the move so if a user tries to hit the V2 url it will redirect to the new and updated V3 url).

Update 10/8/2007: I’ve enhanced the command to take advantage of another new command I created: gl-import2 (object IDs are now maintained during the move so items dependent on a specific ID should no longer break).

Update 10/12/2007: I’ve fixed some issues with the -retainobjectidentity setting and the use of the import2 command. As a result I’ve decided to make the -retainobjectidentity an option specified via a parameter rather than the default behavior. The main issue is due to the fact that when importing sites using the -retainObjectIdentity option the exported site must be a child of the root site or else things get really messed up (as a result I move the site to be under the root site, then export, then import, and then rename the site to match the original plus a possible suffix if the original name already existed at the target). The other issue I found is that for some reason some web parts will be imported twice on a given page – I figure though that this is easier to fix (simply delete the extra web part) than the alternative of having to retarget all the web parts to their new source.

Update 10/17/2007: I’ve made some changes to the gl-moveweb command so that it now supports moving a root level web (that is, converting a site collection to a sub-site). Note that there are limitations when converting a site collection to a sub-site. If you are using the fantastic 40 application templates you’re likely to run into issues (see the comments for gl-convertsubsitetositecollection). Also – if you are moving a root web (site collection) the -retainobjectidentity parameter will not work (an error will be returned if you attempt to use it).