MOSS Powershell widgets (updated 2011-05-31)
In my day job as a MOSS sysadmin, I'm often called upon to do or assist with various tasks which are either tedious or impossible through the web interface. Since it is possible to reference the MOSS libraries directly in Powershell, you can effectively write small console applications - utilising the full "under the hood" power and complexity - against SharePoint with nothing more than a text editor. This area serves as a repository for some of these scripts that I've written, both for my own future reference and anybody else's.
I have a fairly large library of these scripts but I have to put in some work to clean them up, genericise them and strip out the abusive comments, so this list is very much a work in progress.
Notes/Disclaimer
I am not a developer; I know just enough to be dangerous. Use these entirely at your own risk. Take backups prior to running, test in an isolated environment first, yada yada yada.
That said, all of these have been run in my production MOSS environment without any problems. If it didn't work in some obvious way, it wouldn't be here.
All scripts are written against MOSS 2007 Enterprise SP2; no guarantees of compatibility with anything else apply. This is especially true of SharePoint 2010, as the code has not been checked for forward compatibility against the 2010 object model. If you want to try, at the very least you'll need to update the reference to the Microsoft.SharePoint DLL. You should also make sure you're using Powershell 2, as some of the scripts use try/catch which didn't exist in PS1. If you're running on a version of Windows Server with UAC (i.e. 2008/2008 R2), you'll need to run these scripts from an elevated Powershell prompt (right click and do run as Administrator).
You can double-click on the code window to strip out the formatting, leaving a version suitable for copy-and-pasting.
1. Recycle bin flush
MOSS has a "two stage" recycle bin model; the first stage is merely a database flag, and deleting items to (or restoring from) this recycle bin is more or less instantaneous. To actually reclaim the space in the DB however, the items must be deleted entirely. This happens in one of two circumstances: either the retention period specified in Central Admin comes and goes, or the admin manually deletes the items from this recycle bin, which moves them to the second stage (another database flag, so a quick operation), and finally runs a permanent delete against the items, which will actually remove them from the DB.
I have found, in my environment where MOSS is a document management system, that large-scale deletions can cause the second stage recycle bin page to timeout before the delete process has completed for all the items. This is particularly true for big folders or lists of many GB; a few of these in one operation and unless you have a crazy I/O subsystem you can quickly exceed the 30s default page timeout.
This simple script alleviates the issue by doing the deletions via the object model, which will never time out. It's essentially a foreach loop, and it iterates through all the items in a query of the second stage recycle bin, deleting them as it goes. Note that by default it will only return 5000 items in each batch; if you need to, you can increase this number. Also note that you will need to manually delete the items from the first stage recycle bin beforehand, as this script won't do it for you as a safety measure (although it's certainly possible to do it in code).
Show/hide code
#################################################
#
# flushrecyclebin.ps1
#
# Invoke this from a Powershell prompt by calling "./flushrecyclebin.ps1 http://webapp/siteurl".
# This script will only delete items in the second stage (site collection) recycle bin, so you
# will need to manually flush items you wish to delete from the first stage into the second
# stage.
#
#################################################
# we accept this parameter on the command line. If you have several sites' recycle bins to
# flush, you could easily turn this into a foreach loop
param($param_site);
# set this to be an acceptable number of records to delete in one batch. If your recycle bin
# has more items in it than this, you will need to rerun it
$rowlimit = 5000;
#################################################
#
# Don't change below here
#
#################################################
[void] [System.Reflection.Assembly]::Load(”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”)
$site = new-object Microsoft.SharePoint.SPSite("$param_site");
$query = new-object Microsoft.SharePoint.SPRecycleBinQuery;
$query.ItemState = "SecondStageRecycleBin";
$query.RowLimit = $rowlimit;
$itemcoll = $site.GetRecycleBinItems($query);
foreach ($item in $itemcoll) {
$id = $item.ID;
write-host -nonewline "Deleting " $item.Title " ... ";
$itemcoll.Delete($id);
write-host "Done";
}
2. Clean old Choice column values
I wrote this script because we have some document libraries which use "choice" columns extensively. Choice columns are just string fields with a frontend that enforces selection of one of a number of allowed options (if it's a multivalue column, it just concatenates the strings with a semicolon separator). This works fine, until you need to delete one of the options: you can remove it from the picker frontend but not from all the items in the library which have already had that value populated.
This could have been made much more obvious in the UI by Microsoft, and is a pain when you are trying to use MOSS as a bona fide EDMS. I wrote this script to clean up after these kinds of mistakes corrections had been made.
Show/hide code
##################################################
#
# cleanoldchoicevalues.ps1
#
# Loops through all items in a list view and removes any column values that match
# those specified in the oldvals array.
# Designed to clean up after a value was removed from a "choice" column, but items
# with the value were still present in the list.
#
# Use at your own risk.
#
###################################################
# User-modifiable variables
$siteurl = "https://webapp/sitename"; # site collection URL
$weburl = ""; # blank denotes root web of a site collection
$listname = "My list";
$viewname = "PowershellItems"; # create a view that is filtered for the items you want to correct
$columnname = "Activity"; # the column you want to operate on; this code doesn't handle multiple columns in one run but it's certainly possible
$oldvals = @("Baking","Farming"); # Put any values to remove in quotes, comma separated, inside this array
###################################################
#
# Don't change below here
#
###################################################
[void] [System.Reflection.Assembly]::Load(”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”)
write-host ">>> STARTING at "(get-date) `n;
$site = new-object Microsoft.SharePoint.SPSite("$siteurl");
$web = $site.OpenWeb("$weburl");
$list = $web.Lists["$listname"];
$view = $list.Views["$viewname"];
$itemcoll = $list.GetItems($view);
foreach ($item in $itemcoll) {
write-host -NoNewLine "Working on" $item.Name; "...";
$colvals = $item["$columnname"];
$i = 0;
foreach ($remval in $oldvals) {
if ($colvals) {
if ($colvals.Contains("$remval")) {
$item["$columnname"] = $colvals.Replace($remval, "");
write-host " " $remval "removed.";
$i++
}
else {
write-host " " $remval "not found!";
}
}
else {
write-host " Column" $columnname" is null for this item.";
}
}
if ($i -gt 0) {
write-host " "$i" properties udated, updating item...";
$item.UpdateOverwriteVersion();
write-host " done";
}
}
$web.Dispose();
$site.Dispose();
write-host "`n>>> FINISHED at "(get-date);
3. Document Library Versions Report
This script generates a simple report of sorts, which is suitable for identifying frequently-updated or very large files (or worse, files where both apply), where frequent versioning may be contributing to excessive storage utilisation. It is run at the document library level, and dumps out basic information about all the files contained within, irrespective of folder structure. The script is specifically written to be lightweight and performant (it must be capable of running against document libraries with hundreds of thousands of items in my environment), which is why the dataset returned is so basic. If you have the RAM, increase the $query.RowLimit value to allow the script to process more items in one go before going back to the database.
The output is built to be importable into Excel as a CSV (comma-separated values file) by > redirecting the output.
Show/hide code
##################################################
#
# list-itemversions.ps1
#
# When run against a document library, outputs a report on all files contained therein, suitable
# for a CSV comprising the following fields:
# > Full server-relative path to item
# > Modified date of item
# > User who modified item
# > Total number of versions (simple integer count, ignores major/minor status)
# > Size of the current version
#
# The script runs a recursive query, so returns all files in the document library, the equivalent
# to a view using "show all items without folders".
#
# Invoke this script using a > operator to redirect the output to a file,
# as follows:
#
# ./list-itemversions.ps1 > output.csv
#
###################################################
# User-modifiable variables
$siteurl = "https://webapp/sitename"; # site collection URL
$weburl = ""; # blank denotes root web of a site collection
$listurl = "/sitename/My%20List";
###################################################
#
# Don't change below here
#
###################################################
cls;
write-host ">>> STARTING at"(get-date) `n;
[void] [System.Reflection.Assembly]::Load(”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”)
$site = new-object Microsoft.SharePoint.SPSite("$siteurl");
$web = $site.OpenWeb("$weburl");
$list = $web.GetList("$listurl");
$query = new-object Microsoft.SharePoint.SPQuery
$query.RowLimit = 1000; # only read 1000 items at a time into memory. Decrease this if you run into memory issues, or increase for a slight speed gain
$query.ViewAttributes += " Scope='Recursive'"; # this is where we make the query disregard folders
$index = 1;
write-output "Path,Modified Date,Modified By,Num Versions,Current Ver Size (bytes)";
do {
$itemcoll = $list.GetItems($query);
foreach ($item in $itemcoll) {
write-host $($item.Url);
write-output "$($item.Url),$($item["Modified"].ToString()),$($item["Editor"]),$($item.Versions.Count),$($item.File.Length)";
}
$query.ListItemCollectionPosition = $itemcoll.ListItemCollectionPosition;
$index++;
} while ($query.ListItemCollectionPosition -ne $null);
write-host ">>> FINISHED at"(get-date) `n;
4. Delete all old versions
One of the weaknesses of the MOSS (well, WSS) version control model for document libraries is that it defaults to "keep everything forever". This is definitely the safe choice, however it is definitely also a compromise, because it makes some assumptions about your use patterns that you may not fully understand in your haste/excitement to deploy SharePoint. For one thing, if you deploy both major and minor versions, will the users understand the difference - and, more importantly, will they actually use the Publish to Major Version functionality? Because if they don't, even if you've remembered to set limits on the number of versions to retain, the rules will never actually kick in, because they are all predicated on there being at least one major version.
Worse still, if you subsequently realise what's happened months or years down the line, it'll break your heart to find out that even after you put the rules in place, they only take effect the next time each item is modified. This exact situation happened to us, and in a library of 10,000 items, each one having 20-50 versions of up to a megabyte each, it was causing totally unnecessary storage headaches.
The below script is really simple, and just executes a method through the object model which deletes all but the current version of each item in a view of a document library.
Show/hide code
##################################################
#
# delete-allpreviousversions.ps1
#
# When run against a document library view, iterates through all items in the view and deletes
# all but the current version. Note that this deletion BYPASSES the recycle bin.
#
# Use at your own risk.
#
#
###################################################
# User-modifiable variables
$siteurl = "https://webapp/sitename"; # site collection URL
$weburl = ""; # blank denotes root web of a site collection
$listurl = "/sitename/My%20List";
$viewname = "All Documents"
###################################################
#
# Don't change below here
#
###################################################
[System.Reflection.Assembly]::Load(”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”)
cls;
write-host ">>> STARTING at "(get-date) `n;
$site = new-object Microsoft.SharePoint.SPSite("$siteurl");
$web = $site.OpenWeb("$weburl");
$list = $web.GetList["$listurl"];
$view = $list.Views["$viewname"];
$listitems = $list.GetItems($view);
foreach ($listitem in $listitems) {
$listitem.File.Versions.DeleteAll();
# Uncomment the below line if you have a slow SQL Server or I/O subsystem and you're worried
# about all the deletions locking up the database. Pauses for 1 second between list items.
# [System.Threading.Thread]::Sleep(1000);
}
write-host ">>> FINISHED at "(get-date) `n;
5. Deploy custom master page to all site collections
Got a custom master page? Great! Got it wrapped in a solution and triggered by a feature for deployment? Even better! This script will automate the process of pushing out your custom master page (and site logo) to every site collection and web starting at the root and working all the way down (or up, I suppose). I run it every night to ensure that all new sites created through the day pick up our custom branding without any effort necessary from the users. In my environment, that kind of delay (up to 12 hours for a site created early in the morning) is acceptable; if you run a high-traffic Intranet or publically available site, it may not be. But if that's the case, you probably won't be using this kind of ugly hack anyway!
If you have a custom master page, but no solution and no idea how to make one, fear not; you can do it pretty simply with nothing other than a text editor and makecab.exe. You do however have to understand how solutions and features work under the covers, which is something I can't go into here. This is a good starting point, and this applies it to the subject of master pages specifically.
Show/hide code
##################################################
#
# deploy-custommasterpage.ps1
#
# Starting from a web application, applies a uniform master page and site logo to every site; from
# the top on down. Ensures consistent branding when automated and run (e.g.) nightly; if somebody
# creates a site with a non-standard config during the day it will be brought into line overnight.
#
# The catch is that the master page must be packaged up as a solution with an associated feature,
# and deployed to the Farm before this script can work. It's not that hard; you can do it with
# notepad and the command line!
#
###################################################
# Web application GUID; get this from the Central Admin site by hovering over the
# web app in the list and noting the GUID in the IE status bar
$webappguid = "b29e9a9a-4f86-48af-a8c3-64a20fd0ea67";
# The GUID of your master page feature
$feature = "b23aa595-24b5-4872-ab7b-ff6924dab550";
# The filename of the master page you want to assign to all sites
$masterpage = "my_new_masterpage.master";
# The server-relative path to an image you want to make the site logo
$sitelogourl = "/SiteCollectionImages/Logos/mylogo.png";
# Put any URLs to exclude in quotes, comma separated, inside this array
$arrExclude = @("https://webapp/specialsite","https://webapp/intranet/specialsite2");
###################################################
#
# Don't change below here
#
###################################################
[System.Reflection.Assembly]::Load(”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”)
cls;
$webapp = [Microsoft.SharePoint.Administration.SPFarm]::Local.GetObject($webappguid);
foreach ($site in $webapp.Sites) {
if ($arrExclude -notcontains $site.Url) {
write-host -NoNewLine "`nOpening Site Collection "$site.Url"... ";
# AllWebs.Count returns null if we don't have permissions on site
if ($site.AllWebs.Count) {
write-host -NoNewLine "done";
try {
# if the Pub Infra and master page solution aren't deployed on the site, deploy them.
# Pub Infra GUID should be the same for everyone's deployments since it's a MOSS thing
if (!$site.Features.Item("A392DA98-270B-4e85-9769-04C0FDE267AA")) {
write-host -NoNewLine "`n Deploying Publishing Infrastructure feature to site collection...";
$site.Features.Add("A392DA98-270B-4e85-9769-04C0FDE267AA") | Out-Null;
write-host -NoNewLine "done";
}
if (!$site.Features.Item($feature)) {
write-host -NoNewLine "`n Deploying Custom Master Page feature to site collection...";
$site.Features.Add($feature) | Out-Null;
write-host -NoNewLine "done";
}
foreach ($web in $site.AllWebs) {
if ($arrExclude -notcontains $web.Url) {
write-host -NoNewLine "`n Updating master pages and logo url on"$web.Url"... ";
if ($site.ServerRelativeUrl -eq "/") {
$web.MasterUrl = "/_catalogs/masterpage/" + $masterpage;
$web.CustomMasterUrl = "/_catalogs/masterpage/" + $masterpage;
$web.SiteLogoUrl = "$sitelogourl";
$web.Update();
} else {
$web.MasterUrl = $site.ServerRelativeUrl + "/_catalogs/masterpage/" + $masterpage;
$web.CustomMasterUrl = $site.ServerRelativeUrl + "/_catalogs/masterpage/" + $masterpage;
$web.SiteLogoUrl = "$sitelogourl";
$web.Update();
}
write-host -NoNewLine "done";
}
else {
write-host -NoNewLine "`n Excluded URL"$web.Url"due to exclusion list";
}
}
}
catch {
write-host -NoNewLine "FAILED! Error: "$error[0];
}
finally {
$web.Dispose();
}
}
else {
write-host -NoNewLine "FAILED! Cannot open site";
}
$site.Dispose();
}
else {
write-host -NoNewLine "`nExcluded URL"$site.Url"due to exclusion list";
}
}