I finally figured it out! This was supposed to be one of those very simple tasks that I should have been able to do without any custom code. Turning a sub-site (or web) into a site collection (or top level site) turned out to be the most difficult task I’ve yet to face with SharePoint 2007. In theory you should be able to do this using the following commands which could be put into a batch file:
1REM Create a test web for exporting
2stsadm -o createweb -url "http://intranet/testweb" -sitetemplate "SPSTOPIC#0"
3
4REM Export the test web to the filesystem
5stsadm -o export -url "http://intranet/testweb" -filename "c:\testweb" -includeusersecurity -versions 4 -nofilecompression -quiet
6
7REM Create a managed path for the new top level site
8stsadm -o addpath -url "http://intranet/testsite" -type explicitinclusion
9
10REM Create an empty site with a default site template (note that if you don't specify a template you have to manually activate the required features)
11stsadm -o createsite -url "http://intranet/testsite" -owneremail "someone@example.com" -ownerlogin "domain\username" -sitetemplate "SPSTOPIC#0"
12
13REM Import the site
14stsadm -o import -url "http://intranet/testsite" -filename "c:\testweb" -includeusersecurity -nofilecompression -quiet
Unfortunately what you get is only a partially functional site (and in some case not functional at all). There are several errors that you are likely to encounter after running the above using the created testweb or your own existing web. The first and most obvious error is that when you load the default.aspx page of the new site you may get a File Not Found error (note that running the above as is will not give you this error). This is the result of the publishing pages PageLayout URL getting messed up (effectively still pointing to an old value). I addressed this specific issue with a separate command (gl-fixpublishingpagespagelayouturl
) and the I’ve encapsulated that functionality into the new commands I’ve created which are detailed below.
The next error you’re likely to see is on the Area Template Settings page (Site Settings -> Page layouts and site templates). The specific error is “Data at the root level is invalid. Line 1, position 1”.
Anyone who’s done a lot of XML work should recognize this error as an XML parsing error. This error occurs if the web you imported from was set to inherit it’s page layouts from it’s parent. When a web is setup this way there’s a property called __PageLayouts
which gets set to __inherit
.
For a top level site collection this value should always be either an empty string (all page layouts are available) or XML describing which layouts are available. The import operation does not consider this and leaves the value as is thus resulting in the XML error when attempting to parse __inherit
as XML. The fix for this is simple enough – change the value to an empty string. Unfortunately that’s not all we have to do. Fixing the above error results in the page loading without errors, however, the page layouts section does not load. There’s still several issues that need to be resolved. If you now go and view the master gallery (Site Settings -> Master pages and page layouts) you should see all the default page layouts. If you have any custom page layouts those won’t exist and will cause problems.
Also, if you attempt to edit a file you’ll notice that even though we used a publishing template it doesn’t prompt you to check the file out. What’s more is that once you view the form for a layout you should see that only the core fields are present (no Content Type, Associated Content Type, Variations, etc.).
There are several things that need to be fixed here – first we need to activate all the features that would otherwise be activated on a new site collection (need this so that we can get the publishing workflow options enabled). Second we need to reset all the properties for the gallery to match that of our source gallery (namely we need to allow management of content types).
Third we have to change the ContentType
field from being a Text field to being a Choice field (more about this in a minute). Fourth we need to re-associate each file as a Page Layout file by setting all the necessary properties (Content Type, Associated Content Type, etc.). In regards to changing the ContentType
field this is the one that caused me the most headache to figure out. For some reason during the import of the site this field gets a bit messed up (note that I’m not referring to the ContentType
field that is linked in from the Page content type which is associated with the library but rather another field that is part of the gallery definition itself). The field should be a Choice field with options such as “Page Layout”, “Publishing Mater Page”, “Master Page”, and “Folder”. However, during the import the field is converted to a Text field – don’t ask me why.
The result is that when you query for the list of available page layouts the unmanaged code that Microsoft uses to do the actual query chokes because it can’t find a matching value in the list so it produces an invalid query which will always return no results back. To fix this I copy the source master page gallery on top of the target gallery using the content deployment API. I also found that (with SP2) the PublishingResources feature seemed to correct the issue (at least in the tests that I ran).
Update 9/4/2007: I just discovered that another issue is related to the global navigation. If you view the navigation via the browser it will look as though everything is just peachy but if you attempt to manipulate the navigation programmatically you’ll find that the PublishingWeb’s GlobalNavigationNodes property is empty (no items are present). This is because the global navigation is stored at the site collection level so when you import the web it does not take the global navigation with it, just the current. The fix is simple enough – just loop through the current navigation collection and copy it to the global navigation collection. This will help to allow other programmatic manipulations of the global navigation to succeed.
Update 7/6/2009: I am now calling the code that I created to copy content types from one site collection to another. This solves issues that can occur if a site collection content type was not created via a Feature. I’ve also added additional logging to better show what is happening.
So, to summarize the things that need to be repaired after importing into your empty site collection:
- Activate any features that are needed by the site
- Set the master page gallery settings
- Enable content type management
- Set your publishing options
- Fix the Content Type field so that it’s a Choice field type by copying the source master page gallery
- Copy missing content types from the source site collection.
- Fix the Page Layouts and Site Templates
- Set the
__PageLayouts
property to an empty string (can then be set to something else usingSetAvailablePageLayouts()
but first needs to be set to an empty string asSetAvailablePageLayouts()
will not work until it’s fixed) - Copy any missing page layouts from the source
- Set all appropriate properties on each page layout
- Set the
- Fix all the publishing pages
PageLayout
property to have the correct URL - Update 9/4/2007: Update the global navigation
Some of the above can be done via the browser but most of it requires programmatic changes. In order to solve all these problems I’ve created two custom stsadm commands – the first will take a site make all the repairs identified above (so it assumes you’ve already imported the site).
The second basically just abstracts the whole process of exporting a web, creating a site, importing into the site, and then repairing the site (this way the entire process can be done with just one command). The commands I created are detailed below (forgive the verbosity of the names – I had trouble coming up with something shorter).
gl-repairsitecollectionimportedfromsubsite
Here’s the code for the command:
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Collections.ObjectModel;
5using System.Collections.Specialized;
6using System.Data;
7using System.Data.SqlClient;
8using System.Diagnostics;
9using System.Text;
10using System.Text.RegularExpressions;
11using System.Threading;
12using System.Web.UI.WebControls.WebParts;
13using System.Xml;
14using Lapointe.SharePoint.STSADM.Commands.Lists;
15using Lapointe.SharePoint.STSADM.Commands.SPValidators;
16using Lapointe.SharePoint.STSADM.Commands.WebParts;
17using Microsoft.SharePoint;
18using Microsoft.SharePoint.Administration;
19using Microsoft.SharePoint.Deployment;
20using Microsoft.SharePoint.Navigation;
21#if MOSS
22using Microsoft.SharePoint.Publishing;
23using Microsoft.SharePoint.Publishing.Fields;
24using Microsoft.SharePoint.Publishing.Navigation;
25using Microsoft.SharePoint.Publishing.WebControls;
26#endif
27using Microsoft.SharePoint.Utilities;
28using Microsoft.SharePoint.WebPartPages;
29using WebPart=Microsoft.SharePoint.WebPartPages.WebPart;
30using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
31using Lapointe.SharePoint.STSADM.Commands.Features;
32
33namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
34{
35 /// <summary>
36 /// Repairs a site collection that has been imported from an exported sub-site.
37 /// Note that the sourceurl can be the actual source site or any site collection
38 /// that can be used as a model for the target.
39 /// </summary>
40 public class RepairSiteCollectionImportedFromSubSite : SPOperation
41 {
42 public RepairSiteCollectionImportedFromSubSite()
43 {
44 SPParamCollection parameters = new SPParamCollection();
45 parameters.Add(new SPParam("sourceurl", "source", true, null, new SPUrlValidator(), "Please specify the source sub-site or model site collection."));
46 parameters.Add(new SPParam("targeturl", "target", true, null, new SPUrlValidator(), "Please specify the target location of the new site collection to repair."));
47
48 StringBuilder sb = new StringBuilder();
49 sb.Append("\r\n\r\nRepairs a site collection that has been imported from an exported sub-site. Note that the sourceurl can be the actual source site or any site collection that can be used as a model for the target.\r\n\r\nParameters:");
50 sb.Append("\r\n\t-sourceurl <source location of the existing sub-site or model site collection>");
51 sb.Append("\r\n\t-targeturl <target location for the new site collection>");
52
53 Init(parameters, sb.ToString());
54 }
55
56 #region ISPStsadmCommand Members
57
58 /// <summary>
59 /// Gets the help message.
60 /// </summary>
61 /// <param name="command">The command.</param>
62 /// <returns></returns>
63 public override string GetHelpMessage(string command)
64 {
65 return HelpMessage;
66 }
67
68 /// <summary>
69 /// Runs the specified command.
70 /// </summary>
71 /// <param name="command">The command.</param>
72 /// <param name="keyValues">The key values.</param>
73 /// <param name="output">The output.</param>
74 /// <returns></returns>
75 public override int Execute(string command, StringDictionary keyValues, out string output)
76 {
77 output = string.Empty;
78
79 string sourceurl = Params["sourceurl"].Value.TrimEnd('/');
80 string targeturl = Params["targeturl"].Value.TrimEnd('/');
81
82 RepairSite(sourceurl, targeturl, true);
83
84 return OUTPUT_SUCCESS;
85 }
86
87 #endregion
88
89 /// <summary>
90 /// Repairs the site.
91 /// </summary>
92 /// <param name="sourceurl">The sourceurl.</param>
93 /// <param name="targeturl">The targeturl.</param>
94 /// <param name="verbose">if set to <c>true</c> [verbose].</param>
95 public static void RepairSite(string sourceurl, string targeturl, bool verbose)
96 {
97 Verbose = verbose;
98 using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
99 using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
100 using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
101 using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
102 {
103 AddMissingFeatures(sourceSite, sourceWeb, targetSite, targetWeb);
104
105 try
106 {
107 Log("Progress: Begin copying content types...");
108 ContentTypes.CopyContentTypes.Copy(sourceurl, targeturl, verbose);
109 }
110 finally
111 {
112 Log("Progess: End copying content types.");
113 }
114 }
115
116 // We need to re-open all the objects as some values such as content types need to be refreshed.
117 using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
118 using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
119 using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
120 using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
121 {
122 SetMasterPageGallerySettings(sourceSite, targetSite, targetWeb);
123
124#if MOSS
125 PublishingWeb targetPublishingWeb = PublishingWeb.GetPublishingWeb(targetWeb);
126 PublishingSite targetPublishingSite = new PublishingSite(targetSite);
127 PublishingWeb sourcePublishingWeb = PublishingWeb.GetPublishingWeb(sourceWeb);
128 PublishingSite sourcePublishingSite = new PublishingSite(sourceSite);
129
130 FixPageLayoutsAndSiteTemplates(sourcePublishingSite, sourcePublishingWeb, targetPublishingSite, targetPublishingWeb, targetSite, targetWeb);
131#endif
132 }
133
134 TimerJob.ExecAdmSvcJobs.Execute(false, true);
135
136 using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
137 using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
138 using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
139 using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
140 {
141#if MOSS
142 PublishingWeb targetPublishingWeb = PublishingWeb.GetPublishingWeb(targetWeb);
143
144 FixPublishingPages(targetSite);
145
146 SetGlobalNavigation(targetPublishingWeb);
147
148 RetargetGroupedListingsWebPart.RetargetGroupedListings(targetWeb, "grouped listings");
149#endif
150 //RepairDiscussionLists(targetSite);
151
152 RetargetMiscWebParts(sourceSite, sourceWeb, targetWeb);
153 }
154 }
155
156 #region Worker Methods
157
158 #region Retargart Miscellaneous Web Parts
159
160 /// <summary>
161 /// Retargets the misc web parts.
162 /// </summary>
163 /// <param name="sourceSite">The source site.</param>
164 /// <param name="sourceWeb">The source web.</param>
165 /// <param name="targetWeb">The target web.</param>
166 internal static void RetargetMiscWebParts(SPSite sourceSite, SPWeb sourceWeb, SPWeb targetWeb)
167 {
168 StringDictionary listMap = new StringDictionary();
169
170 // Handle the primary web first then address the child webs
171 FindMatchingLists(listMap, sourceWeb, targetWeb);
172
173 string parentTargetUrl = targetWeb.ServerRelativeUrl;
174 string parentSourceUrl = sourceWeb.ServerRelativeUrl;
175
176 RetargetMiscWebPartsRecursiveHelper(listMap, parentSourceUrl, parentTargetUrl, sourceSite, targetWeb);
177
178 RetargetMiscWebParts(targetWeb, listMap);
179 }
180
181 /// <summary>
182 /// Recursive helper for retargetting the misc web parts.
183 /// </summary>
184 /// <param name="listMap">The list map.</param>
185 /// <param name="parentSourceUrl">The parent source URL.</param>
186 /// <param name="parentTargetUrl">The parent target URL.</param>
187 /// <param name="sourceSite">The source site.</param>
188 /// <param name="targetWeb">The target web.</param>
189 private static void RetargetMiscWebPartsRecursiveHelper(StringDictionary listMap, string parentSourceUrl, string parentTargetUrl, SPSite sourceSite, SPWeb targetWeb)
190 {
191 foreach (SPWeb childTargetWeb in targetWeb.Webs)
192 {
193 try
194 {
195 string targetParentRelativeUrl = childTargetWeb.ServerRelativeUrl.Substring(parentTargetUrl.Length).TrimEnd('/');
196 string sourceUrl = Utilities.ConcatServerRelativeUrls(parentSourceUrl, targetParentRelativeUrl).TrimEnd('/');
197
198
199 using (SPWeb childSourceWeb = sourceSite.AllWebs[sourceUrl])
200 {
201 if (childSourceWeb.Exists)
202 {
203 FindMatchingLists(listMap, childSourceWeb, childTargetWeb);
204 }
205 }
206 RetargetMiscWebPartsRecursiveHelper(listMap, parentSourceUrl, parentTargetUrl, sourceSite, childTargetWeb);
207
208 }
209 finally
210 {
211 childTargetWeb.Dispose();
212 }
213 }
214 }
215
216 /// <summary>
217 /// Finds the matching lists.
218 /// </summary>
219 /// <param name="listMap">The list map.</param>
220 /// <param name="sourceWeb">The source web.</param>
221 /// <param name="targetWeb">The target web.</param>
222 private static void FindMatchingLists(StringDictionary listMap, SPWeb sourceWeb, SPWeb targetWeb)
223 {
224 foreach (SPList targetList in targetWeb.Lists)
225 {
226 try
227 {
228 // Try to find a matching list in the source web
229 SPList sourceList = sourceWeb.Lists[targetList.Title];
230 listMap.Add(sourceList.ID.ToString(), targetList.ID.ToString());
231 }
232 catch (ArgumentException)
233 {
234 // There is no matching source list - this should happen if our source is the actual source and not a model.
235 // If the source is just a model then we're wasting our time here but unfortunately there's no way to
236 // determine that without asking the user.
237 }
238 }
239 }
240
241 /// <summary>
242 /// Retargets the misc web parts for the target web. Loops through all sub-webs and all files.
243 /// </summary>
244 /// <param name="targetWeb">The target web.</param>
245 /// <param name="listMap">The list map.</param>
246 private static void RetargetMiscWebParts(SPWeb targetWeb, StringDictionary listMap)
247 {
248 foreach (SPWeb subweb in targetWeb.Webs)
249 {
250 try
251 {
252 RetargetMiscWebParts(subweb, listMap);
253 }
254 finally
255 {
256 subweb.Dispose();
257 }
258 }
259
260 foreach (SPFile file in targetWeb.Files)
261 {
262 RetargetMiscWebParts(targetWeb, listMap, file);
263 }
264
265 foreach (SPList list in targetWeb.Lists)
266 {
267 foreach (SPListItem item in list.Items)
268 {
269 if (item.File != null && item.File.Url.ToLowerInvariant().EndsWith(".aspx"))
270 {
271 RetargetMiscWebParts(targetWeb, listMap, item.File);
272 }
273 }
274 }
275 targetWeb.Dispose();
276 }
277
278 /// <summary>
279 /// Retargets the misc web parts on a specific file. Loops through all web parts on the file. Currently only
280 /// <see cref="DataFormWebPart"/> and <see cref="ContentByQueryWebPart"/> are considered.
281 /// </summary>
282 /// <param name="targetWeb">The target web.</param>
283 /// <param name="listMap">The list map.</param>
284 /// <param name="file">The file.</param>
285 private static void RetargetMiscWebParts(SPWeb targetWeb, StringDictionary listMap, SPFile file)
286 {
287 if (file == null)
288 {
289 return; // This should never be the case.
290 }
291 if (!Utilities.EnsureAspx(file.Url, true, false))
292 return; // We can only handle aspx and master pages.
293
294 if (file.InDocumentLibrary && Utilities.IsCheckedOut(file.Item) && !Utilities.IsCheckedOutByCurrentUser(file.Item))
295 {
296 return; // The item is checked out by a different user so leave it alone.
297 }
298
299 bool fileModified = false;
300 bool wasCheckedOut = true;
301
302 SPLimitedWebPartManager manager = null;
303 try
304 {
305 manager = targetWeb.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
306 SPLimitedWebPartCollection webParts = manager.WebParts;
307
308 for (int i = 0; i < webParts.Count; i++)
309 {
310 WebPart webPart = null;
311 try
312 {
313 webPart = webParts[i] as WebPart;
314 if (webPart == null)
315 {
316 webParts[i].Dispose();
317 continue;
318 }
319
320#if MOSS
321 if (!(webPart is ContentByQueryWebPart || webPart is DataFormWebPart))
322 {
323 webPart.Dispose();
324 continue;
325 }
326#else
327 if (!(webPart is DataFormWebPart))
328 {
329 webPart.Dispose();
330 continue;
331 }
332#endif
333 foreach (string sourceId in listMap.Keys)
334 {
335 bool modified = false;
336
337 ReplaceWebPartContent.Settings settings = new ReplaceWebPartContent.Settings();
338 settings.LogFile = null;
339 settings.Quiet = true;
340 settings.Test = false;
341 settings.SearchString = string.Format("(?i:{0})", sourceId);
342 settings.ReplaceString = listMap[sourceId];
343 settings.Publish = true;
344 settings.UnsafeXml = true;
345
346 Regex regex = new Regex(settings.SearchString);
347
348 // As every web part has different requirements we are only going to consider a small subset.
349 // Custom web parts will not be addressed as there's no interface that can utilized.
350#if MOSS
351 if (webPart is ContentByQueryWebPart)
352 {
353 ContentByQueryWebPart wp = (ContentByQueryWebPart)webPart;
354 webPart = ReplaceWebPartContent.ReplaceValues(
355 targetWeb,
356 file,
357 settings,
358 wp,
359 regex,
360 ref manager,
361 ref wasCheckedOut,
362 ref modified);
363 }
364 else
365#endif
366 if (webPart is DataFormWebPart)
367 {
368 DataFormWebPart wp = (DataFormWebPart)webPart;
369 webPart = ReplaceWebPartContent.ReplaceValues(
370 targetWeb,
371 file,
372 settings,
373 wp,
374 regex,
375 ref manager,
376 ref wasCheckedOut,
377 ref modified);
378 }
379
380 if (modified)
381 manager.SaveChanges(webPart);
382
383 if (modified)
384 fileModified = true;
385 }
386 }
387 finally
388 {
389 if (webPart != null)
390 webPart.Dispose();
391 }
392 }
393 if (fileModified)
394 file.CheckIn(
395 "Checking in changes to list item due to retargetting of web part as a result of converting a sub-site to a site collection.");
396
397 if (file.InDocumentLibrary && fileModified && !wasCheckedOut)
398 PublishItems.PublishListItem(file.Item, file.Item.ParentList,
399 new PublishItems.Settings(true, false, null),
400 "\"stsadm.exe -o repairsitecollectionimportedfromsubsite | convertsubsitetositecollection\"");
401 }
402 finally
403 {
404 if (manager != null)
405 {
406 manager.Web.Dispose(); // manager.Dispose() does not dispose of the SPWeb object and results in a memory leak.
407 manager.Dispose();
408 }
409 }
410 }
411
412 #endregion
413
414#if MOSS
415
416 #region Set Global Navigation
417
418 /// <summary>
419 /// Sets the global navigation.
420 /// </summary>
421 /// <param name="targetPublishingWeb">The target publishing web.</param>
422 private static void SetGlobalNavigation(PublishingWeb targetPublishingWeb)
423 {
424 SPNavigationNodeCollection globalNodes = targetPublishingWeb.GlobalNavigationNodes;
425 SPNavigationNodeCollection currentNodes = targetPublishingWeb.CurrentNavigationNodes;
426
427 if (globalNodes.Count > 0)
428 {
429 return;
430 }
431
432 SetGlobalNavigationRecursiveHelper(currentNodes, globalNodes);
433 }
434
435 /// <summary>
436 /// Recursive routine for setting the global navigation (necessary to handle child elements).
437 /// </summary>
438 /// <param name="currentNodes">The current nodes.</param>
439 /// <param name="globalNodes">The global nodes.</param>
440 private static void SetGlobalNavigationRecursiveHelper(SPNavigationNodeCollection currentNodes, SPNavigationNodeCollection globalNodes)
441 {
442 // We're going to copy the current nodes in order to set the global nodes (so use current as the default)
443 for (int i = 0; i < currentNodes.Count; i++)
444 {
445 SPNavigationNode currentNode = currentNodes[i];
446 NodeTypes type = NodeTypes.None;
447 if (currentNode.Properties["NodeType"] != null)
448 type = (NodeTypes)Enum.Parse(typeof(NodeTypes), (string)currentNode.Properties["NodeType"]);
449
450 SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
451 currentNode.Title, currentNode.Url, type, globalNodes);
452 foreach (DictionaryEntry de in currentNode.Properties)
453 {
454 node.Properties[de.Key] = de.Value;
455 }
456 node.Update();
457 node.MoveToLast(globalNodes);
458
459 if (currentNode.Children.Count > 0)
460 SetGlobalNavigationRecursiveHelper(currentNode.Children, node.Children);
461 }
462 }
463
464 #endregion
465
466
467 /// <summary>
468 /// Fixes the publishing pages.
469 /// </summary>
470 /// <param name="targetSite">The target site.</param>
471 private static void FixPublishingPages(SPSite targetSite)
472 {
473 Log("Progress: Begin fixing publishing pages...");
474 try
475 {
476 foreach (SPWeb web in targetSite.AllWebs)
477 {
478 try
479 {
480 if (!PublishingWeb.IsPublishingWeb(web))
481 continue;
482
483 PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
484
485 Pages.FixPublishingPagesPageLayoutUrl.FixPages(pubweb);
486 }
487 finally
488 {
489 web.Dispose();
490 }
491 }
492 }
493 finally
494 {
495 Log("Progress: End fixing publishing pages.");
496 }
497 }
498
499 /// <summary>
500 /// Fixes the page layouts and sets the site templates.
501 /// </summary>
502 /// <param name="sourcePublishingSite">The source publishing site.</param>
503 /// <param name="sourcePublishingWeb">The source publishing web.</param>
504 /// <param name="targetPublishingSite">The target publishing site.</param>
505 /// <param name="targetPublishingWeb">The target publishing web.</param>
506 /// <param name="targetSite">The target site.</param>
507 /// <param name="targetWeb">The target web.</param>
508 private static void FixPageLayoutsAndSiteTemplates(PublishingSite sourcePublishingSite, PublishingWeb sourcePublishingWeb, PublishingSite targetPublishingSite, PublishingWeb targetPublishingWeb, SPSite targetSite, SPWeb targetWeb)
509 {
510 try
511 {
512 Log("Progress: Begin fixing page layouts and site templates...");
513 // Next thing we need to do is reset the "__PageLayouts" property - after doing the import
514 // this value (targetWeb.AllProperties["__PageLayouts"]) will equal "__inherit" but it should
515 // be either an empty string or an xml list of layouts. Fixing this resolves the xml error
516 // that we receive when going to "targetSite/_layouts/AreaTemplateSettings.aspx":
517 // "Data at the root level is invalid. Line 1, position 1". This error occurs when GetAvailablePageLayouts()
518 // is called - within that method there's call to get to GetEffectiveAvailablePageLayoutsAsString()
519 // which in this case returns back "__inherit" but should return back "" - as a result the next
520 // statement after that is a call to IsPropertyAllowingAll - this returns back false because it's not
521 // an empty string - as a result an XmlDocument.LoadXml() call is made which fails because "__inherit"
522 // is not valid xml.
523
524 if (!string.IsNullOrEmpty(targetWeb.AllProperties["__PageLayouts"] as string))
525 {
526 Log(
527 "Progress: Setting \"__PageLayouts\" web property to string.Empty (current value is \"{0}\")...",
528 targetWeb.AllProperties["__PageLayouts"] as string);
529 targetWeb.AllProperties["__PageLayouts"] = string.Empty;
530 // We need to update the web so that the changes above stick and the following code can execute.
531 targetWeb.Update();
532 }
533
534 // Reset to make sure everything gets propagated correctly. (Note - this may need to be moved to after we copy the page layouts)
535 Log("Progress: Setting available page layouts...");
536 if (sourcePublishingWeb.IsAllowingAllPageLayouts)
537 targetPublishingWeb.AllowAllPageLayouts(true);
538 else
539 targetPublishingWeb.SetAvailablePageLayouts(sourcePublishingWeb.GetAvailablePageLayouts(), true);
540
541 // Reset the site templates settings.
542 if (sourcePublishingWeb.IsAllowingAllWebTemplates)
543 {
544 Log("Progress: Allowing all site templates...");
545 targetPublishingWeb.AllowAllWebTemplates(true);
546 }
547 else
548 {
549 Log("Progress: Setting explicit site template settings...");
550 // Handle site templates set explicitly.
551 Collection<SPWebTemplate> list = new Collection<SPWebTemplate>();
552 foreach (SPWebTemplate template in sourcePublishingWeb.GetAvailableCrossLanguageWebTemplates())
553 {
554 list.Add(template);
555 }
556 targetPublishingWeb.SetAvailableCrossLanguageWebTemplates(list, true);
557
558 foreach (SPLanguage lang in sourcePublishingWeb.Web.RegionalSettings.InstalledLanguages)
559 {
560 list = new Collection<SPWebTemplate>();
561 foreach (
562 SPWebTemplate template in sourcePublishingWeb.GetAvailableWebTemplates((uint) lang.LCID))
563 {
564 list.Add(template);
565 }
566 if (list.Count > 0)
567 targetPublishingWeb.SetAvailableWebTemplates(list, (uint) lang.LCID, true);
568 }
569 }
570
571 Log("Progress: Setting page layouts...");
572 foreach (PageLayout layout in sourcePublishingSite.GetPageLayouts(false))
573 {
574 // We need to reset all the page layouts to match that of the source site
575 // (after the import all the layouts are messed up and treated as plain files and not page layouts
576 // because the content type is no longer associated).
577 PageLayoutCollection targetSiteLayouts = targetPublishingSite.GetPageLayouts(false);
578 PageLayout tempPageLayout = null;
579
580 try
581 {
582 tempPageLayout = targetSiteLayouts[
583 targetPublishingSite.PageLayouts.LayoutsDocumentLibrary.RootFolder.ServerRelativeUrl.
584 TrimStart('/') + "/" + layout.Name];
585 }
586 catch (ArgumentException)
587 {
588 }
589
590 if (tempPageLayout == null)
591 {
592 // We didn't find an item in the collection so let's attempt to add it back.
593 Log("Progress: Source layout {0} not found in target layout collection, searching files...",
594 layout.Name);
595 // First we need to see if the file is already there.
596 SPFile file = null;
597 foreach (SPListItem item in targetPublishingSite.PageLayouts.LayoutsDocumentLibrary.Items)
598 {
599 if (item.Name == layout.Name)
600 {
601 // We found the file so exit out of the loop.
602 file = item.File;
603 Log("Progress: Layout file found.");
604 break;
605 }
606 }
607
608 if (file == null)
609 {
610 Log("Progress: Layout file Not found.");
611 SPList targetMasterGallery =
612 targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
613 SPFolder targetMasterGalleryFolder = targetMasterGallery.RootFolder;
614
615 try
616 {
617 Log("Progress: Copying page layout {0} from source site...", layout.Name);
618 // We couldn't find a file so copy the file from the source.
619 file = targetMasterGalleryFolder.Files.Add(layout.Name,
620 layout.ListItem.File.OpenBinary());
621 }
622 catch (Exception ex)
623 {
624 Log(
625 string.Format("ERROR: Unable to copy page layout from source location - {0}",
626 ex.Message), EventLogEntryType.Error);
627 continue;
628 }
629 }
630 if (file == null)
631 {
632 Log("WARNING: Unable to copy page layout from source location.", EventLogEntryType.Warning);
633 continue;
634 }
635
636 // Reset all the properties on the file so that it's flagged as a PageLayout content type.
637 Log("Progress: Setting page layout properties...");
638 SPListItem listItem = file.Item;
639 listItem[FieldId.Hidden] = layout.ListItem[FieldId.Hidden];
640 listItem[FieldId.Title] = layout.ListItem[FieldId.Title];
641 listItem[FieldId.ContentType] =
642 GetContentType(targetSite.RootWeb, ContentTypeId.PageLayout).Name;
643 listItem[FieldId.AssociatedContentType] =
644 new ContentTypeIdFieldValue(GetContentType(targetSite.RootWeb,
645 layout.AssociatedContentType.Id));
646 listItem[FieldId.AssociatedVariations] = layout.ListItem[FieldId.AssociatedVariations];
647 listItem[SPBuiltInFieldId.File_x0020_Type] = layout.ListItem[SPBuiltInFieldId.File_x0020_Type];
648
649 // Add any additional properties specific to the page layout
650 PageLayout newLayout = new PageLayout(listItem);
651 newLayout.Description = layout.Description;
652 newLayout.Title = layout.Title;
653 if (!string.IsNullOrEmpty(layout.PreviewImageUrl))
654 {
655 string previewImageUrl =
656 targetWeb.ServerRelativeUrl +
657 layout.PreviewImageUrl.Substring(
658 layout.PreviewImageUrl.IndexOf("/_catalogs/"));
659 newLayout.PreviewImageUrl = targetSite.MakeFullUrl(previewImageUrl);
660 }
661 listItem.SystemUpdate();
662
663 if (Utilities.IsCheckedOut(file.Item))
664 {
665 Log("Progress: Checking in page layout file...");
666 file.CheckIn("", SPCheckinType.MajorCheckIn);
667 if (file.Item.ModerationInformation != null)
668 file.Approve("");
669 }
670 }
671 }
672 }
673 finally
674 {
675 Log("Progress: End fixing page layouts and site templates.");
676 }
677 }
678#endif
679
680 /// <summary>
681 /// Adds the missing features.
682 /// </summary>
683 /// <param name="sourceSite">The source site.</param>
684 /// <param name="sourceWeb">The source web.</param>
685 /// <param name="targetSite">The target site.</param>
686 /// <param name="targetWeb">The target web.</param>
687 public static void AddMissingFeatures(SPSite sourceSite, SPWeb sourceWeb, SPSite targetSite, SPWeb targetWeb)
688 {
689 Log("Progress: Begin adding missing features...");
690 try
691 {
692 // Set any features that need to be enabled.
693 Dictionary<SPFeatureScope, SPFeatureCollection> activeFeatures =
694 new Dictionary<SPFeatureScope, SPFeatureCollection>();
695 if (sourceSite != null)
696 {
697 activeFeatures[SPFeatureScope.WebApplication] = sourceSite.WebApplication.Features;
698 activeFeatures[SPFeatureScope.Site] = sourceSite.Features;
699 }
700 if (sourceWeb != null)
701 activeFeatures[SPFeatureScope.Web] = sourceWeb.Features;
702
703 // Note that you should be able to use the ActivationDependencies property of the SPDefinition,
704 // however, I found that this property is not reliable (good example: Publishing is dependent
705 // on PublishingSite but when you view the ActivationDependencies for the Publishing definition
706 // it shows zero dependencies.
707 Queue<SPFeatureDefinition> queuedFeatures = new Queue<SPFeatureDefinition>();
708 // For some reason these need to be added before the rest...
709 if (SPFarm.Local.FeatureDefinitions["BaseSite"] != null)
710 queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["BaseSite"]);
711 if (SPFarm.Local.FeatureDefinitions["PremiumSite"] != null)
712 queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["PremiumSite"]);
713 if (SPFarm.Local.FeatureDefinitions["PublishingSite"] != null)
714 queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["PublishingSite"]);
715
716 foreach (SPFeatureDefinition definition in SPFarm.Local.FeatureDefinitions)
717 {
718 try
719 {
720 if (definition.Scope == SPFeatureScope.Farm)
721 continue;
722
723 if (!queuedFeatures.Contains(definition))
724 queuedFeatures.Enqueue(definition);
725 }
726 catch (Exception ex)
727 {
728 Log("ERROR: {0}", ex.Message);
729 }
730 }
731 while (queuedFeatures.Count > 0)
732 {
733 SPFeatureDefinition definition = queuedFeatures.Dequeue();
734 if (definition == null)
735 continue;
736
737 SPFeatureScope scope = SPFeatureScope.ScopeInvalid;
738 try
739 {
740 scope = definition.Scope;
741 }
742 catch (Exception ex)
743 {
744 Log("ERROR: {0}", ex.Message);
745 continue;
746 }
747 Guid featureID = definition.Id;
748 if (activeFeatures[scope] != null)
749 {
750 bool isActive = (activeFeatures[scope][featureID] != null);
751 if (!isActive)
752 continue;
753
754 try
755 {
756 switch (scope)
757 {
758 case SPFeatureScope.Site:
759 if (targetSite != null && targetSite.Features[featureID] == null)
760 {
761 Log("Progress: Activating site scoped feature \"{0}\"...", definition.DisplayName);
762 targetSite.Features.Add(featureID);
763 }
764 break;
765 case SPFeatureScope.Web:
766 if (targetWeb != null && targetWeb.Features[featureID] == null)
767 {
768 Log("Progress: Activating web scoped feature \"{0}\"...", definition.DisplayName);
769 targetWeb.Features.Add(featureID);
770 }
771 break;
772 case SPFeatureScope.WebApplication:
773 if (sourceSite != null && sourceSite.WebApplication.Features[featureID] == null)
774 {
775 Log("Progress: Activating web application scoped feature \"{0}\"...", definition.DisplayName);
776 sourceSite.WebApplication.Features.Add(featureID);
777 }
778 break;
779 default:
780 continue;
781 }
782 }
783 catch (ArgumentOutOfRangeException)
784 {
785 // We couldn't add the item most likely due a dependent content type that has not yet been added.
786 queuedFeatures.Enqueue(definition);
787 }
788 catch (InvalidOperationException)
789 {
790 // We couldn't add the item most likely due to dependencies so add to the back of the queue.
791 queuedFeatures.Enqueue(definition);
792 }
793 catch (ArgumentException)
794 {
795 // We couldn't add the item most likely due to dependencies so add to the back of the queue.
796 queuedFeatures.Enqueue(definition);
797 }
798 catch (SPException ex)
799 {
800 // This can occur occasionally if the feature does some external modifications (should be rare)
801 if (ex.Message == "The web being updated was changed by an external process.")
802 {
803 if (targetWeb != null)
804 targetWeb.Close();
805 if (targetSite != null)
806 targetWeb = targetSite.OpenWeb();
807 queuedFeatures.Enqueue(definition);
808 }
809 else
810 {
811 Log("WARNING: Unable to activate feature '{0} ({1})'\r\n{2}",
812 definition.DisplayName, definition.Name, ex.Message);
813 }
814 }
815 }
816 }
817 }
818 finally
819 {
820 Log("Progress: End adding missing features.");
821 }
822 }
823
824
825 /// <summary>
826 /// Sets the master page gallery settings.
827 /// </summary>
828 /// <param name="sourceSite">The source site.</param>
829 /// <param name="targetSite">The target site.</param>
830 /// <param name="targetWeb">The target web.</param>
831 internal static void SetMasterPageGallerySettings(SPSite sourceSite, SPSite targetSite, SPWeb targetWeb)
832 {
833 Log("Progress: Begin setting master page gallery settings...");
834 try
835 {
836 SPList targetMasterGallery = targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
837 SPList sourceMasterGallery = sourceSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
838
839 // Need to make sure the master gallery has it's content types enabled (get's disabled during the import)
840 // and make sure all other settings are set to match the source.
841 Log("Progress: Setting versioning properties...");
842 targetMasterGallery.ContentTypesEnabled = true;
843 targetMasterGallery.EnableModeration = sourceMasterGallery.EnableModeration;
844 targetMasterGallery.EnableVersioning = sourceMasterGallery.EnableVersioning;
845 targetMasterGallery.EnableMinorVersions = sourceMasterGallery.EnableMinorVersions;
846 targetMasterGallery.MajorVersionLimit = sourceMasterGallery.MajorVersionLimit;
847 try
848 {
849 targetMasterGallery.MajorWithMinorVersionsLimit = sourceMasterGallery.MajorWithMinorVersionsLimit;
850 }
851 catch (NotSupportedException)
852 {
853 // If we're here then something is wrong with the source list so just ignore the error.
854 }
855 targetMasterGallery.DraftVersionVisibility = sourceMasterGallery.DraftVersionVisibility;
856 targetMasterGallery.ForceCheckout = sourceMasterGallery.ForceCheckout;
857
858 targetMasterGallery.Update();
859
860 SPField contentTypeField = targetMasterGallery.Fields.GetFieldByInternalName("ContentType");
861 if (contentTypeField.Type != SPFieldType.Choice)
862 {
863 Log("Progress: ContentType field must be a Choice column (currently is {0})", contentTypeField.TypeAsString);
864 if (contentTypeField.Type == SPFieldType.Text)
865 {
866#if MOSS
867 if (PublishingWeb.IsPublishingWeb(targetWeb))
868 {
869 Log("Progress: ContentType field is a Text type in a publishing web, attempting to activate PublishingResources to correct...");
870 Guid publishingResourcesFeatureId = new Guid("AEBC918D-B20F-4a11-A1DB-9ED84D79C87E");
871 FeatureHelper fh = new FeatureHelper();
872 fh.ActivateDeactivateFeatureAtSite(targetSite, true, publishingResourcesFeatureId, true, false);
873
874 // Get the list again to make sure we're not dealing with a cached copy
875 targetMasterGallery = targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
876 contentTypeField = targetMasterGallery.Fields.GetFieldByInternalName("ContentType");
877 if (contentTypeField.Type != SPFieldType.Choice)
878 {
879 Log("Progress: Failed to fix ContentType field via PublishingResources feature activation, attempting to copy entire gallery from source...");
880 CopyList cp = new CopyList();
881 cp.Copy(sourceSite.MakeFullUrl(sourceMasterGallery.RootFolder.ServerRelativeUrl),
882 targetSite.Url, null, true, false, true, true, false, false, SPIncludeVersions.All, SPUpdateVersions.Overwrite, true, false, false, false, false);
883
884 }
885 }
886#endif
887
888 }
889 }
890 }
891 finally
892 {
893 Log("Progress: End setting master page gallery settings.");
894 }
895 }
896
897 #endregion
898
899 #region Helper Methods
900
901 /// <summary>
902 /// Gets the type of the content.
903 /// </summary>
904 /// <param name="web">The web.</param>
905 /// <param name="contentTypeId">The content type id.</param>
906 /// <returns></returns>
907 private static SPContentType GetContentType(SPWeb web, SPContentTypeId contentTypeId)
908 {
909 SPContentType type = web.AvailableContentTypes[contentTypeId];
910 if (type == null)
911 {
912 throw new SPException("Content Type Not Found In Web " + contentTypeId + ", " + web.Url);
913 }
914 return type;
915 }
916
917 /// <summary>
918 /// Gets the folder by id.
919 /// </summary>
920 /// <param name="list">The list.</param>
921 /// <param name="id">The id.</param>
922 /// <returns></returns>
923 private static SPListItem GetFolderById(SPList list, int id)
924 {
925 foreach (SPListItem folder in list.Folders)
926 {
927 if (id == folder.ID)
928 return folder;
929 }
930 return null;
931 }
932 #endregion
933 }
934}
The syntax of the command can be seen below:
C:\>stsadm -help gl-repairsitecollectionimportedfromsubsite
stsadm -o gl-repairsitecollectionimportedfromsubsite
Repairs a site collection that has been imported from an exported sub-site. Note that the sourceurl can be the actual source site or any site collection that can be used as a model for the target.
Parameters:
-sourceurl <source location of the existing sub-site or model site collection>
-targeturl <target location for the new site collection>
The following table summarizes the command and its various parameters:
Command Name | Availability | Build Date |
---|---|---|
gl-repairsitecollectionimportedfromsubsite | WSS 3, MOSS 2007 | Released: 9/4/2007, Updated: 7/6/2009 |
Parameter Name | Short Form | Required | Description | Example Usage |
---|---|---|---|---|
sourceurl | source | Yes | The URL to the source sub-site to convert. | -sourceurl http://portal/subsite , -source http://portal/subsite |
targeturl | target | Yes | The URL of the new site collection to create. | -targeturl http://portal/sites/site , -target http://portal/sites/site |
Here’s an example of how to repair the site created using the batch file above:
stsadm –o gl-repairsitecollectionimportedfromsubsite –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/"
gl-convertsubsitetositecollection
As stated above, this command is just an abstraction of other commands – it simply calls out to stsadm to do export the site (note that you can provide a previously exported site file/folder), create the managed path, create the empty site, import the site, and finally repair the imported site. As there’s nothing spectacular going on here I didn’t bother culling the code out in this post (download the project if you’re interested in the details). The syntax of the command can be seen below:
C:\>stsadm -help gl-convertsubsitetositecollection
stsadm -o gl-convertsubsitetositecollection
Converts a sub-site to a top level site collection via a managed path.
Parameters:
-sourceurl <source location of the existing sub-site or model site collection>
-targeturl <target location for the new site collection>
-owneremail <someone@example.com>
[-createmanagedpath]
[-haltonwarning]
[-haltonfatalerror]
[-includeusersecurity]
[-suppressafterevents (disable the firing of "After" events when creating or modifying list items)]
[-exportedfile <filename of exported site if previously exported>]
[-nofilecompression]
[-ownerlogin <DOMAIN\name>]
[-ownername <display name>]
[-secondaryemail <someone@example.com>]
[-secondarylogin <DOMAIN\name>]
[-secondaryname <display name>]
[-lcid <language>]
[-title <site title>]
[-description <site description>]
[-hostheaderwebapplicationurl <web application url>]
[-quota <quota template>]
[-deletesource]
[-createsiteinnewdb]
[-createsiteindb]
[-databaseuser <database username>]
[-databasepassword <database password>]
[-databaseserver <database server name>]
[-databasename <database name>]
[-verbose]
The following table summarizes the command and its various parameters:
Command Name | Availability | Build Date |
---|---|---|
gl-convertsubsitetositecollection | WSS 3, MOSS 2007 | Released: 9/4/2007, Updated: 7/6/2009 |
Parameter Name | Short Form | Required | Description | Example Usage |
---|---|---|---|---|
sourceurl | source | Yes | The URL to the source sub-site to convert. | -sourceurl http://portal/subsite , -source http://portal/subsite |
targeturl | target | Yes | The URL of the new site collection to create. | -targeturl http://portal/sites/site , -target http://portal/sites/site |
owneremail | oe | Yes | The site owner’s e-mail address. Must be valid e-mail address, in the form someone@example.com. | -owneremail someone@example.com , -oe someone@example.com |
createmanagedpath | createpath | No | Create a new managed path for the site collection. | -createmanagedpath , -createpath |
haltonwarning | warning | No | Stop execution of the command if a warning event occurs during the export or import process. | -haltonwarning , -warning |
haltonfatalerror | error | No | Stop execution of the command if a fatal error occurs during the export or import process. | -haltonfatalerror , -error |
exportedfile | file | No | Use a previously exported site (created using stsadm’s export command). | -exportedfile c:\exportdata\site , -file c:\exportdata\site |
nofilecompression | No | Do not compress the site when exporting (or if previously exported use an uncompressed file for the import). | -nofilecompression | |
ownerlogin | ol | If your farm does not have Active Directory account creation mode enabled, then this parameter is required. This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address. | The site owner’s user account. Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name . | -ownerlogin domain\name , -ol domain\name |
ownername | on | No | The site owner’s display name. | -ownername "Gary Lapointe" , -on "Gary Lapointe" |
secondaryemail | se | No | The secondary site owner’s e-mail address. Must be valid e-mail address, in the form someone@example.com. | -secondaryemail someone@example.com , -se someone@example.com |
secondarylogin | sl | If your farm does not have Active Directory account creation mode enabled, then this parameter is required. This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address. | The secondary site owner’s user account. Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name . | -secondarylogin domain\name , -sl domain\login |
secondaryname | sn | No | The secondary site owner’s display name. | -secondaryname "Pam Lapointe" , -sn "Pam Lapointe" |
lcid | No | A valid locale ID, such as “1033” for English. You must specify this parameter when using a non-English template. | -lcid 1033 | |
title | t | No | The title of the new site collection (this value will be overwritten when the site is imported – it is available only to help in situations in which the import fails). | -title "New Site" |
description | desc | No | Description of the site collection (this value will be overwritten when the site is imported – it is available only to help in situations in which the import fails). | -description "New Site Description" , -desc "New Site Description" |
hostheaderwebapplicationurl | hhurl | No | A valid URL assigned to the Web application by using Alternate Access Mapping (AAM), such as “http://server_name”. When the hostheaderwebapplicationurl parameter is present, the value of the url parameter is the URL of the host-named site collection and value of the hostheaderwebapplicationurl parameter is the URL of the Web application that will hold the host-named site collection. | -hostheaderwebapplicationurl http://newsite , -hhurl http://newsite |
quota | No | The quota template to apply to sites created on the virtual server. | -quota Portal | |
deletesource | No | Delete the source site after conversion (only recommended if significant testing has occurred). | -deletesource | |
createsiteinnewdb | newdb | No | Create the site collection in a content database. | -createsiteinnewdb , -newdb |
createsiteindb | db | No | Create the site collection in an existing content database. | -createsiteindb , -db |
databaseserver | ds | No | The database server containing the specified content database. If not specified then the default database server is used. | -databaseserver spsql1 , -ds spsql1 |
databaseuser | du | No | The administrator user name for the SQL Server database. | -databaseuser domain\user , -du domain\user |
databasepassword | dp | No | The password that corresponds to the administrator user name for the SQL Server database. | -databasepassword password , -dp password |
databasename | dn | Yes if createsiteinnewdb or createsiteindb is specified. | The name of the content database to put the site collection in (will be created if createsiteinnewdb is specified). | -databasename SharePoint_Content1 , -db SharePoint_Content1 |
suppressafterevents | sae | No | Disable the firing of After events when creating or modifying files or list items during the import. | -suppressafterevents , -sae |
verbose | v | No | Displays logging information when executing. | -verbose , -v |
Here’s an example of how to do all that the batch file above is doing (minus the creation of the testweb) as well as the repair operation all with one command:
stsadm –o gl-convertsubsitetositecollection –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/" -createmanagedpath -nofilecompression -owneremail "someone@example.com" -ownerlogin "domain\user" -deletesource
One area of improvement may be to pull the owner and secondary owner information from the source site collection so that this information does not have to be provided – maybe I’ll do that if I feel I have the time or if people express enough interest. Note that you can specify a title and description but they’ll be overwritten during the import – I only included them so that if the import fails and you’re left with an incomplete site you’ll at least have a name for it if you should forget to delete it and stumble upon it a year later.
Figuring out how to solve all the issues surrounding converting a web to a site collection was a real pain the a$$ so any feedback that people have on this would be greatly appreciated – hopefully if there are others out there that have stumbled on this then they’ll benefit from it as well. Keep in mind also that though I think I’ve solved all the errors related to the conversion it’s possible that different implementations may have additional errors that I have not seen – if that’s the case please let me know (especially if you’ve solved the problems) so that I can share with others.
Update 9/21/2007: I’ve fixed a couple minor bugs that pop up when converting a non-publishing site. I’ve also 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/2/2007: I’ve enhanced the command to take advantage of another new command I created: gl-retargetcontentquerywebpart
(fixes Grouped Listings web parts that remained pointed at the old list rather than the newly imported list).
Update 10/12/2007: I’ve removed the -retainobjectidentity
parameter. If you attempt to use this parameter you will receive a syntax error. Turns out that retaining the object identity when going from a sub-site to a site collection just created a nightmare. However, because I still had to handle these web parts that were broken I decided to enhance the repair routines to manually retarget the DataFormWebPart
and ContentByQueryWebPart
web parts. So if a matching list can be found on the source then any of these web parts on your pages should be fixed so that you don’t have to manually fix them (the gl-repairsitecollectionimportedfromsubsite
command will do the same).
Update 7/6/2009: I removed the direct DB access code and added support for copying content types from the source.