PowerShell: Script to Update SharePoint Content Type Document Template


Introduction

One of the great things about Content Types are that you can have a document template associated to the Content Type.

This means that when you create a new document a customised word template with its own look and feel can be used to guide the user through the process of filling the document and setting up the content type’s metadata.

Recently, we had the situation where we had content types being used across lots of site collections each for the different departments in the organisation.

One of the management overheads is when a document template needs to be updated. How can you do that programmatically using PowerShell?

Well the following script helps you get that done.

Solution

The PowerShell Script, Update-SPContentTypeDocumentTemplate.ps1 below does just that.

param
(
[Parameter(Mandatory=$true, HelpMessage='Url for Site Collection hosting content type')]
[string]$Url = "https://sharepoint.ithinksharepoint.com",
[Parameter(Mandatory=$true, HelpMessage='Name of the Content Type to apply document template to')]
[string]$ContentTypeName = "Policy Document",
[Parameter(Mandatory=$true, HelpMessage='File path of the document template to upload')]
[string]$DocumentTemplatePath = "",
[Parameter(Mandatory=$false, HelpMessage='Filename to store the file in SharePoint as')]
[string]$DocumentTemplateFileName = "",
[Parameter(Mandatory=$false, HelpMessage='Should derived child content types and list content types be updated?')]
[boolean]$UpdateChildren = $true
)
$site=Get-SPSite $Url;
if($site -ne $null)
{
$rootWeb = Get-SPWeb $Url;
$contentType = $rootWeb.ContentTypes | ?{$_.Name -eq $ContentTypeName};
if($contentType -ne $null -and [System.IO.File]::Exists($DocumentTemplatePath))
{
$templateFile = [System.IO.File]::OpenRead($DocumentTemplatePath);
$memoryStream = new-object System.IO.MemoryStream;
$templateFile.CopyTo($memoryStream);
if([String]::IsNullOrEmpty($DocumentTemplateFileName))
{
$DocumentTemplateFileName = [System.IO.Path]::GetFileName($DocumentTemplatePath);
}
$destinationUrl=[String]::Format("{0}/{1}/{2}", $rootWeb.ServerRelativeUrl, $contentType.ResourceFolder.Url, $DocumentTemplateFileName);
if(-not [String]::IsNullOrEmpty($contentType.DocumentTemplateUrl))
{
Write-Host "Checking Content Type Document Template Url: $contentType.DocumentTemplateUrl for file" -ForegroundColor White;
$checkFile = $rootWeb.GetFile($contentType.DocumentTemplateUrl);
if($checkFile.Exists)
{
Write-Host "File found at $contentType.DocumentTemplateUrl, removing" -ForegroundColor White;
$checkFile.Delete();
}
}
Write-Host "Checking for file @ $destinationUrl" -ForegroundColor White;
$checkFile=$rootWeb.GetFile($destinationUrl);
if($checkFile.Exists)
{
Write-Host "file found @ $destinationUrl, removing" -ForegroundColor White;
$checkFile.Delete();
}
Write-Host "Content Type Document Template not set or found " $contentType.Name -ForegroundColor White;
$checkFile = $contentType.ResourceFolder.Files.Add($destinationUrl, $memoryStream.ToArray());
$checkFile.Update();
Write-Host "Uploaded file $DocumentTemplatePath -> $destinationUrl for" $contentType.Name -ForegroundColor White;
$contentType.DocumentTemplate = $DocumentTemplateFileName;
$contentType.UpdateIncludingSealedAndReadOnly($UpdateChildren);
Write-Host "Applied Document Template $destinationUrl to " $contentType.Name " and updated content type (All Children Update Flag set? $UpdateChildren)" -ForegroundColor Green;
}
else{
Write-Error "Cannot find $ContentTypeName in $Url or cannot find document template file $DocumentTemplatePath, please check.";
}
}
else
{
Write-Host "Cannot find Site $Url" -ForegroundColor Yellow;
}
Write-Host "Finished";

So how do you use it?

Well you provide the URL of the site, the path to the document template and also the name of the Content Type.

It will access the site, check if the content type already has a document template, upload the selected document and configure the content type to use the new document template.

You can decide if you want to update all the child content types which have been assigned to document libraries. if you want to do this which is recommended then make sure you include the -UpdateChild switch.

Example

.\Update-SPContentTypeDocumentTemplate.ps1 –Url https://sharepoint -ContentTypeName "Policy Document" -DocumentTemplatePath "c:\dev\documents\policydocumenttemplate.dotx" -DocumentTemplateFileName "policydocumenttemplatev2.dotx" -UpdateChildren

So what would happen if you wanted to update a number of sites?

Well you can just get a list of the sites and pipe that array into the command and it will update the content type on each site.

Also the same technique could be used to update multiple content types on a number of sites.

Download Script

The PowerShell Script is part of the iThink SharePoint PowerShell GitHub Repository, which can be found here:

https://github.com/ithinksharepoint/PowerShell

Anyway, I hope you find the solution useful, please let me know if you did and also if you have some additions or changes then please share them!

All the best

Simon

PowerShell: Deleting SharePoint List Items


Introduction

Whilst I love SharePoint Workflows and how versatile they can be, they can generate quite a bit of data. Well mine do as I like to log plenty of information so that the support / admin teams can find out what’s going on with the workflow.

Unfortunately when you log plenty of information this means that the workflow history list can get quite large.

One of the workflows that we built over a ten month period has processed a couple of hundred thousand list items and has created about 3 million list items in the workflow history list.

We wanted to clear down this list and so PowerShell came to the rescue.

Solution

We built the following PowerShell script which you provide the following parameters:0

  • Url – Url of web hosting the workflow history list
  • AgeOfItemsToDelete – days of logs that you wish to keep
  • ListName – the display name of the workflow history list
  • NumberOfItemsInBatch – the number of items that should be returned in each query.

The original script looked like this:-

param
(
	[Parameter(Mandatory=$false, HelpMessage='System Host Url')]
	[string]$Url = "http://sharepoint",
	[Parameter(Mandatory=$false, HelpMessage='List Name')]
	[string]$ListName = "Workflow Tasks",
	[Parameter(Mandatory=$false, HelpMessage='Age of items in list to keep (number of days).')]
	[int]$AgeOfItemsToKeepInDays = 365,
	[Parameter(Mandatory=$false, HelpMessage='What size batch should we delete the items in?')]
	[int]$NumberOfItemsToDeleteInBatch = 1000
	
)

$assignmentCollection = Start-SPAssignment -Global;

$rootWeb=Get-SPWeb $Url -AssignmentCollection $assignmentCollection;

$listToProcess = $rootWeb.Lists.TryGetList($ListName);
if($listToProcess -ne $null)
{
	$startTime = [DateTime]::Now;
	$numberOfDaysToDelete = [TimeSpan]::FromDays($AgeOfItemsToKeepInDays);
	$deleteItemsOlderThanDate = [DateTime]::Now.Subtract($numberOfDaysToDelete);
	$isoDeleteItemsOlderThanDate = [Microsoft.SharePoint.Utilities.SPUtility]::CreateISO8601DateTimeFromSystemDateTime($deleteItemsOlderThanDate);
	$numberOfItemsToRetrieve = $NumberOfItemsToDeleteInBatch;
	
	$camlQueryString = [String]::Format("<Where><Leq><FieldRef Name='Modified' /><Value IncludeTimeValue='TRUE' Type='DateTime'>{0}</Value></Leq></Where>", $isoDeleteItemsOlderThanDate);
	$camlQuery = New-Object -TypeName "Microsoft.SharePoint.SPQuery" -ArgumentList @($listToProcess.DefaultView);
	$camlQuery.Query=$camlQueryString;
	$camlQuery.RowLimit=$numberOfItemsToRetrieve;
	
	$deletedItemCount=0;
	
	do
	{
		$camlResults = [Microsoft.SharePoint.SPListItemCollection] $listToProcess.GetItems($camlQuery);
		$itemsCountReturnedByQuery = $camlResults.Count;
		Write-Host "Executed Query and found " $camlResults.Count " Items";
		
		$listItemDataTable = [System.Data.DataTable]$camlResults.GetDataTable();
		foreach($listItemRow in $listItemDataTable.Rows)
		{
			$listItemIdToDelete = $listItemRow["ID"];
			$listItemModifiedDate = $listItemRow["Modified"];
			Write-Host "Deleting Item $listItemIdToDelete - Modified $listItemModifiedDate";
			$listItemToDelete = $listToProcess.GetItemById($listItemIdToDelete);
			$listItemToDelete.Delete();
			$deletedItemCount++;
		}
	}
	while($itemsCountReturnedByQuery -gt 0)
	
	$totalSecondsTaken = [DateTime]::Now.Subtract($startTime).TotalSeconds;
	Write-Host -ForegroundColor Green "Processing took $totalSecondsTaken seconds to delete $deletedItemCount Item(s).";
}
else
{
	Write-Host "Cannot find list: " $ListName;
}

Stop-SPAssignment -Global -AssignmentCollection $assignmentCollection;

Write-Host "Finished";

However, whilst this worked ok for a list that was quite small. When we went to use it on the Production environment it performed like a dog. Fortunately the script was run out of hours so didn’t impact the environment too much. Though the memory that it consumed was quite large (4GB) after deleting the second item.

There was something seriously wrong with approach being taken, so after a bit of investigation it was obvious what was going on.

Look at the script again, there is a line of code that is:-

$listToProcess.Items.DeleteItemById($listItemIdToDelete);

Well it turns out that this call, updates the collection after the DeleteItemById function is called. So we made a small modification and the offensive line became:-

$listItemToDelete = $listToProcess.GetItemById($listItemIdToDelete);
$listItemToDelete.Delete();

This change meant that the PowerShell session now only consumed 270Mb (I say only!) and memory usage did not rise. The deletion of the items was much quicker too, probably by a few 1000%!

Here is the final script for completeness.

param
(
[Parameter(Mandatory=$false, HelpMessage='System Host Url')]
[string]$Url = "<a href="http://sharepoint&quot;">http://sharepoint"</a>,
[Parameter(Mandatory=$false, HelpMessage='List Name')]
[string]$ListName = "Workflow Tasks",
[Parameter(Mandatory=$false, HelpMessage='Age of items in list to keep (number of days).')]
[int]$AgeOfItemsToKeepInDays = 365,
[Parameter(Mandatory=$false, HelpMessage='What size batch should we delete the items in?')]
[int]$NumberOfItemsToDeleteInBatch = 1000

)

$assignmentCollection = Start-SPAssignment -Global;

$rootWeb=Get-SPWeb $Url -AssignmentCollection $assignmentCollection;

$listToProcess = $rootWeb.Lists.TryGetList($ListName);
if($listToProcess -ne $null)
{
$startTime = [DateTime]::Now;
$numberOfDaysToDelete = [TimeSpan]::FromDays($AgeOfItemsToKeepInDays);
$deleteItemsOlderThanDate = [DateTime]::Now.Subtract($numberOfDaysToDelete);
$isoDeleteItemsOlderThanDate = [Microsoft.SharePoint.Utilities.SPUtility]::CreateISO8601DateTimeFromSystemDateTime($deleteItemsOlderThanDate);
$numberOfItemsToRetrieve = $NumberOfItemsToDeleteInBatch;

$camlQueryString = [String]::Format("&lt;Where&gt;&lt;Leq&gt;&lt;FieldRef Name='Modified' /&gt;&lt;Value IncludeTimeValue='TRUE' Type='DateTime'&gt;{0}&lt;/Value&gt;&lt;/Leq&gt;&lt;/Where&gt;", $isoDeleteItemsOlderThanDate);
$camlQuery = New-Object -TypeName "Microsoft.SharePoint.SPQuery" -ArgumentList @($listToProcess.DefaultView);
$camlQuery.Query=$camlQueryString;
$camlQuery.RowLimit=$numberOfItemsToRetrieve;

$deletedItemCount=0;

do
{
$camlResults = [Microsoft.SharePoint.SPListItemCollection] $listToProcess.GetItems($camlQuery);
$itemsCountReturnedByQuery = $camlResults.Count;
Write-Host "Executed Query and found " $camlResults.Count " Items";

$listItemDataTable = [System.Data.DataTable]$camlResults.GetDataTable();
foreach($listItemRow in $listItemDataTable.Rows)
{
$listItemIdToDelete = $listItemRow["ID"];
$listItemModifiedDate = $listItemRow["Modified"];
Write-Host "Deleting Item $listItemIdToDelete - Modified $listItemModifiedDate";
$listItemToDelete = $listToProcess.GetItemById($listItemIdToDelete);
$listItemToDelete.Delete();
$deletedItemCount++;
}
}
while($itemsCountReturnedByQuery -gt 0)

$totalSecondsTaken = [DateTime]::Now.Subtract($startTime).TotalSeconds;
Write-Host -ForegroundColor Green "Processing took $totalSecondsTaken seconds to delete $deletedItemCount Item(s).";
}
else
{
Write-Host "Cannot find list: " $ListName;
}

Stop-SPAssignment -Global -AssignmentCollection $assignmentCollection;

Write-Host "Finished";

Hope that helps someone who has the same problem. Please let me know if you have an alternative solution!

Links to the scripts:-

Delete-ListItemsOlderThan-Slow.txt

Delete-ListItemsOlderThanV2.txt