MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Monday, September 17, 2007

Move Web

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:

   1: using (SPSite sourceSite = new SPSite(url))
   2: using (SPWeb sourceWeb = sourceSite.OpenWeb())
   3: using (SPSite parentSite = new SPSite(parentUrl))
   4: using (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>
   8: public 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>
  70: private 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>
  85: private 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).

43 comments:

Alexander Henkel (ahenkel@netik.de) said...

Looking for a solution to move sps webs, I found your blog this matching solution. But I must say that I found a missing advise. You need to AllowUnsafeUpdates to do this operation.

Gary Lapointe said...

Could you elaborate on why you would need to set this property to true? I've looked at all the code Microsoft uses (via Reflector) to move a web site and they do not set this property. I'm also hesitant to ever set this based on the information in the reference pages for the property (http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.spweb.allowunsafeupdates.aspx) - specifically this little blurb: "Setting this property to true opens security risks, potentially introducing cross-site scripting vulnerabilities". At this point I've had no issues during any of the testing I've done so unless it can be shown otherwise I would strongly caution anyone against using this property.

Gary Lapointe said...

I just did a bit more searching (probably should have done that before responding with my original comment :)) and found this post: http://glorix.blogspot.com/2006/07/to-allow-updates-on-get-set.html. In essense - the AllowUnsafeUpdates is only necessary when updating the SPWeb object during an HTTP GET Request - it is not necessary when updating the SPWeb object via the command line. I admit that I'm not 100% on what this property is all about but I feel pretty confident that it's not needed in a console app context (unfortunately the code for this property exist in an unmanaged method so I can't get more info on my own and haven't had a lot of time to research it).

paula said...

I realize this is an old post, but I just stumbled across your site today looking for a solution. I must say the list of commands is impressive, but I'm not certain which I should really use to accomplish what I'm trying to do.

I have a web application that has been upgraded from wss2.0. It is working, but the entire site is one site collection. I'd like to change the top site, which essentially has nothing in it, to a portal site, then change each of the next level subsites so that they go into their own content databases.

I had originally thought import and export, but your set of commands looks intriguing. I'm leaning towards import2, but since i have several subwebs under the first level subwebs, I don't think it will do what I hope. I'm also assuming that I just use the regular export to create the files for the import2 to use?

Does this make sense?

Gary Lapointe said...

You can use import2 if you're not reparenting the site otherwise just use import. If you do decide to use import2 then the built-in export is what you use (just as with the import command). moveweb will also work for you if you're just trying to move a sub-site or convert a top level site to a sub-site. Note though that you can't have a site collection span multiple databases so if you want each sub-site to be in it's own database then each of them will have to be a site collection in which case you can use the convertsubsitetositecollection command.

Nick said...

Gary

Each time I run the import I get this error:

[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/AllItems.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/Combine.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/DispForm.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/EditForm.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/repair.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/template.doc.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/Upload.aspx.
[4/16/2008 3:37:11 PM]: Progress: Importing File Service Contract Renewals/Forms/WebFldr.aspx.
[4/16/2008 3:37:12 PM]: FatalError: Value cannot be null.
Parameter name: g
at System.Guid..ctor(String g)
at Microsoft.SharePoint.Deployment.FieldTemplateSerializer.FixLookupFieldSchema(XmlNode fieldNode, Guid parentWebId, Guid fieldId)
at Microsoft.SharePoint.Deployment.FieldTemplateSerializer.CreateField(SPWeb web, SerializationInfoHelper infoHelper)
at Microsoft.SharePoint.Deployment.FieldTemplateSerializer.SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
at Microsoft.SharePoint.Deployment.XmlFormatter.ParseObject(Type objectType, Boolean isChildObject)
at Microsoft.SharePoint.Deployment.XmlFormatter.DeserializeObject(Type objectType, Boolean isChildObject, DeploymentObject envelope)
at Microsoft.SharePoint.Deployment.XmlFormatter.Deserialize(Stream serializationStream)
at Microsoft.SharePoint.Deployment.ObjectSerializer.Deserialize(Stream serializationStream)
at Microsoft.SharePoint.Deployment.ImportObjectManager.ProcessObject(XmlReader xmlReader)
at Microsoft.SharePoint.Deployment.SPImport.DeserializeObjects()
at Microsoft.SharePoint.Deployment.SPImport.Run()

Any ideas?

Gary Lapointe said...

Nick - The issue you are having is because you have a lookup field somewhere which incorrectly has the WebId not defined in the field schema. Take a look at the schema for your lookup fields in your "Service Contract Renewals" list.

Dominik said...

Hi Gary,

at first, let me say thanks for the great work you are doin for us out here :)

I'm tryin to use your extensions to move Webs to different site collections, but i have problem with the gl-moveweb command.

Let me first explain our server environment. The test machine is an english moss server with german and czech language packs. The basic clsid is 1033 for the server.

And this is, where the problem lies. Your move web command always creates a new web with the 1033 clsid, no matter what the clsid of the source web was.

so for example.

i created a site with german clsid 1031 http://testserver/Test

Then i try to move the web to http://testserver/SITES/TestGerman,which has also been created with the 1031 CLSID.

The Export works great, but as soon as the import of the site starts, i receive the following error message.


C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN>stsa
dm -o gl-moveweb -url http://testserver/test -parenturl http://testserver/sit
es/testgerman -includeusersecurity -haltonfatalerror

Exporting Web...

Web exported.

Creating web for import...
Web created.

Importing web...
Cannot import site. The exported site is using language 1031 but the destination site is using language 1033. You can import sites only into sites that are usin
g same language as the exported site.

Error occured importing site.


so my question is, is it possible to change the gl-moveweb that way that it recognizes the clsid of the source site and creates the target web with the corresponding clsid?

Tia for your help :)

Greetings from Austria

Gary Lapointe said...

Dominik - I was defaulting the locale ID to that of the host site collection - I've changed my code so that it now uses the ID of the source web (not really sure I why I had coded it that way in the first place). Anyways - I've pushed out a new build - thanks for the feedback!

Dominik said...

hi Gary,

i came back here to see if there is something new. As you can imagine I was really surprised to see, that you have already solved my problem.

I just gave it a try and i worked like a charm.

Thank you very much for the implementation, i really appreciate that and i would like you to know that you just safed my day :)

Keep up the great work, and thanks again for your help.

best Greetings

Eli Robillard said...

Hi Gary, nicely done! I don't see the use of the cabsize parameter on the export though, so you're likely limited to sizes under ~24 MB. Suggest you add it if you post future updates.

Cheers,
-Eli.

Gary Lapointe said...

Eli - thanks for the feedback - I wasn't aware of the 24mb limit but the reason this command has always worked for me is that I'm not using file compression (I went back and forth on whether I should and in the end more times than not I wanted to be able to see the resultant manifest files and manipulate them if need be so I decided to not use compression - it wouldn't take much to change it so that this is an option but for my needs no compression was sufficient).

Rwoods said...

Gary, I am looking at moving a subsite to make it the site collection: i.e. I want to move http://server/cww to http://server

how do I do this?

Gary Lapointe said...

Take a look at my gl-convertsubsitetositecollection command.

Anonymous said...

G'day Gaz,

Basiclly looking for a quick solution..

I want to pick up our production enviroment and dump it on our test enviroment..

Is there a way I can use your great tools to achieve this without taking a farm backup and restore.

Dan.

Gary Lapointe said...

Dan - unfortunately all of my copy related commands are scoped to work within a single farm. You can try exporting and importing using the ootb export/import commands or you can use the backup/restore commands. Another option is to copy the content database.

Roger said...

Date and created by problem.
When using gl-moveweb the created by, and created date changes to the System User and the date the moving took place.

This is important information, is there a solution for this issue ?

Gary Lapointe said...

Try the includeusersecurity flag - all this command is doing is wrapping the import/export commands which use the content deployment API - kind of at the mercy of its limitations.

Anonymous said...

Gary - We are trying to use the gl-moveweb to move a site to new site collection. The log follows:

stsadm -o -gl-moveweb -url http://SourceHost/verkefni -parenturl http://NewHost -retainobjectidentity

Exporting Web...

Web exported.

Deleting source web...
Source web deleted.

Importing web...

Log file generated:
c:\...\import.log

The file cannot be imported because its parent web /Verkefni does not exists

This means that the source web has been exported and deleted ... but not imported into the new location.

a) Can you help me on the reason for this, and

b) How can I restore the deleted source web again?

/Sigvaldi

Gary Lapointe said...

Problem is that you used the retainobjectidentity so it had to delete the source. You should be able to import the exported site (it keeps it in the folder with the log file) to the new location use "stsadm -o import ..." or you can use "stsadm -o gl-import2 -retainobjectidentity ..." to import back to the original source location and preserve all your IDs. The retainobjectidentity doesn't really work if you're moving a sub-site unless the target parent site was imported in the same fashion (thus the IDs would be the same).

Allan Walker said...

We are trying to find some way to copy a web (sub-site) from one site (site collection) to another for SDL purposes. (e.g. move web from stage to production) It appears that your gl-movweb command can do a lot of what we need although I have a question as to whether the workflows are also maintained in the move. Do the workflows stay in tact for a web moved from one site to another?

Gary Lapointe said...

The gl-moveweb command will not work between farms - only within the same farm. Basically all I'm doing is wrapping calls to export/import which, as far as I'm aware, will not preserve workflows.

Robbo said...

Gary,
I am trying to move a site from http://intra2/CAG to http://intra2/BS/Cag

I have tried using your gl-moveweb and I've also tried the move command in the content and structure screen in moss, but I get the same error message either way.

The move happens sucessfully, but when I try to view the default.aspx page I get a "cannot complete this action" error message. Some other folk have been able to either move the site, or fix this error by exporting and importing, but whenever I try and export the site stsadm uses up all the memory on the server and starts throwing error messages in the export. The site I am trying to move is over 7gb.

Any ideas how to fix the cannot complete this action error message?

Cheers,
Rob

Gary Lapointe said...

The moveweb command is basically just wrapping the export/import commands when going from one site collection to another - seems like it would fail in the same way as you are describing happens when using the import/export directly. I've seen that cannot complete action error in a lot of places and it's always some cryptic, very odd thing that causes the error. The most common one I've seen is when there's an exception thrown in one of the delegate controls or an inline server script block. You might want to check and see if you can at least access the settings page (_layouts/settings.aspx) - from there make sure you master page is set right and verify your page layouts (try creating a new page if you can). Another thing you can try is to use backup/restore and if that fails then try exporting out individual lists/libraries to reduce the size of the site and thus the export.

Leon Zandman said...

I'd like to see the option to skip the deletion of the source site when moving between site collections. So maybe a "gl-copyweb"?

Leon Zandman said...

For some sites I'm getting this error:

"The form submission cannot be processed because it exceeded the maximum length allowed by the Web administrator. Please resubmit the form with less data."

Any ideas?

Gary Lapointe said...

See this support article: http://support.microsoft.com/kb/822059

John said...

I love the Move Web command. To get completely lazy, it would be excellent if your command also updated the original parent's Top Bar and Quick Link to remove the old links to the site and add them (if they were, in fact, in the top bar and Quick links) to the new parent site.

VThinkTank said...

Hi Gary,
Thank you
Would be nice if you can have before and after screenshots of the apply themes and welcome page items.

Gary Lapointe said...

VThinkTank - I'm not following what you are looking for.

Leon Zandman said...

Some of the sites I moved using your command became pretty messy. This was due to event receivers being triggered during the move operation, which wrongly updated some of the list items in the moved site.

Maybe you should consider adding an extra command line parameter that controls the SPImportSettings.SuppressAfterEvents property. This property can be used to prevent event receivers from being called.

Gary Lapointe said...

Leon - I think that's a great idea about exposing that property as a parameter - I've updated all of my import commands to now include this as a parameter. Thanks for the feedback!

Anonymous said...

Does the command stsadm -o gl-moveweb just make a copy of the source web and put it some target location or will I lose the original web?

Gary Lapointe said...

It just wraps the export/import/deleteweb commands (so, no it's not a copy).

Chris Beach said...

Hello Gary,

First, like others have stated, your bevy of stsadm commands has helped me immensely. Thank you very much for all your contributions!

I've been testing your gl-moveweb command and it is working beautifully. One hesitation I have is that when running the command it deletes the old web, which of course makes this process so succinct; however it seems if the import goes bad I’m out of luck without a web in either location. When it does the export where does it put the exported files? Does your command delete these files after the process has completed? Is there a way to set the path for the export files?

TIA
chris

Gary Lapointe said...

Currently it does delete the exported files. As it's just a wrapper for other commands you could achieve what you are looking for by calling those commands directly (or by making your own version of the moveweb command).

Syndrical.One said...

Hi Gary,

Great tools you have! I've been using them at client sites for a few months now and they make life so much easier!!

One question though: I'm moving a site collection to a sub-site but getting the error "Could not find WebTemplate #1 with LCID 1033". The site collection is using a custom site template, that was based off of team sites.

Any idea how to get around this?

Thanks!

Gary Lapointe said...

Does your custom site definition still exist?

Syndrical.One said...

Hi Gary,

Sorry for the delay as I though tI was going to get an alert or somethig when you replied. Wishful thinking :-P

Yes, my custom site definition still exists

Anonymous said...

Hi,
Your Post is absolutely superb... Congratzzz

We are facing a problem. We would like to Restore a German Site Collection in to an English Site Collection. While restoring the backup the english site collection is getting converted to German. Is there any way in which we can avoid it?? We need the English Site Collection in English but the content and group users must be restored from German Backup.

Thanks
Binu

Gary Lapointe said...

I think you're going to be stuck writing something custom. I see all kinds of localization issues doing it using the deployment API (which is what I'm using).

Anonymous said...

This command isn't available in SharePoint 2010?

Gary Lapointe said...

I haven't gotten around to migrating this one to 2010 yet.