The following problem is a great example of one of those SharePoint situations where you imagine that the job at hand will be relatively straightforward. But that sadly turns on you  with some unexpectedly unsupported scenarios that require going deep down the SharePoint API rabbit hole. These are the original requirements.

  • Given a Site Collection with dummy site defined within it.
  • The user should be able to modify the dummy site and store it as a template.
  • The user has to be able to create any number of sites, which will replicate the structure of the template itself.
  • This structure includes any number of lists and sample data, i.e. placeholders
  • The permissions must be kept exactly like in the the template. The permissions might not necessarily be "inherited".

    Well, covering the first 4 bullets points is pretty average SP fare. However, none of the standard mechanisms to create templates or export site definitions will respect a custom permission structure that breaks the default inheritance.

These are the standardish methods that I initially tried when attempting to replicate the site templates.

And here's the workaround that I put together with the help of some PowerShell. There is still a bit of manual processing, but it is infinitely better than having to define all permissions from scracth.

  1. Create a root site with the desired structure and content
  2. Save a template of the site with the standard SharePoint method (bullet point #1 in the previous list)
  3. Create the document libraries and define their custom permissions
  4. Create a new site from the template saved in step #2. (we are of course, missing the libraries created in step #3)
  5. Use the PowerShell script below to copy and replicate the structure of the libraries defined in step #3

Repeat steps 4 and 5 for any other sites that apply.
To configure the script you only need to change its very last line, which is a call to a recursive function.

CopyDocumentLibrary "http://sharepoint-site-url/site-collection" "Name of Template Library" "http://sharepoint-site-url/site-collection/new-site-name"
Function CopyDocumentLibrary($sourceSite, $sourceFolder, $targetSite){

if($sourceFolder.EndsWith("/Forms")) {
      return #do not perform any actions inside the default "Forms" folder
}

 #find the root folder i.e. Document Library itself
 $index = $sourceFolder.indexOf("/")
 if($index -gt 0){
     $root = $sourceFolder.Substring(0,$index)
     $current = $sourceFolder.Substring($index+1)
 }
 else {
     $root = $sourceFolder
     $current = $root
 }

 $listTemplate = [Microsoft.SharePoint.SPListTemplateType]::DocumentLibrary
 $sourceWeb = Get-SPWeb $sourceSite
 $sourceList = $sourceWeb.Lists[$root] #TODO - check if the sourceList exists
 $targetWeb = Get-SPWeb $targetSite

 if($current.CompareTo($root) -eq 0){
     if($targetWeb.Lists[$root] -eq $null){
         $guid = $targetWeb.Lists.Add($root,"",$listTemplate) #create the root container on the target site
         $qlNav = $targetWeb.Navigation.QuickLaunch
         $qlHeading = $qlNav | where { $_.Title -eq "Libraries" }
         $linkNode = New-Object Microsoft.SharePoint.Navigation.SPNavigationNode($root,$targetWeb.Lists[$root].DefaultViewUrl )
         $qlHeading.Children.AddAsLast($linkNode)
     }
     $targetList = $targetWeb.Lists[$root]
 }
 else {
     $targetList = $targetWeb.Lists[$root]
     if($targetList.Folders[$sourceFolder] -eq $null){
         $newFolder = $targetList.AddItem("",[Microsoft.SharePoint.SPFileSystemObjectType]::Folder,$current)
         $fromFolder = $sourceWeb.GetFolder($sourceFolder)
         if($fromFolder.Item.HasUniqueRoleAssignments)
         {
             $newFolder.Update()
             $newFolder.BreakRoleInheritance($false)
             $newFolder.RoleAssignments.Remove(0) #deletes the role assignment for the implicit user running the script
             foreach($role in $fromFolder.Item.RoleAssignments)
             {
                 $newFolder.RoleAssignments.Add($role)
             }
          }
         $newFolder.Update()
     }
 }
 $files = $sourceWeb.GetFolder($sourceFolder).Files
 $targetFolder = $targetWeb.GetFolder($sourceFolder).Files

 foreach ($file in $files) {
     $targetFile = $targetFolder.Folder.Url + "/" + $file.Name
     $buffer = $file.OpenBinary();
     $targetFolder.Add($targetFile,$buffer,$true)
 }

 $folders = $sourceWeb.GetFolder($sourceFolder).SubFolders
 foreach ($folder in $folders) {
     CopyDocumentLibrary $sourceSite $folder.Url $targetSite
 }

$sourceWeb.dispose()
$targetWeb.dispose()
}

CopyDocumentLibrary "http://sharepoint-site-url/site-collection" "Name of Template Site" "http://sharepoint-site-url/site-collection/new-site-name"

You can also download the zipped version of the script.