Download and synchronize documents from SharePoint using CSOM

I have created a script in 2015 to download all documents from a site collection to a file share or local drive (blogpost) but we now received the request to download and synchronize documents from SharePoint Online. The customer wants to download all documents from one library and keep the documents updated on the file share. SharePoint Online will be the source location where users will be working daily and the file share will be the destination with files max 1 day old. The customer also wanted to use a list field to categorize the documents and place them in folders on the file share. This would make it easier to find the files. I’ve created this script with CSOM using PowerShell for SharePoint Online but it can be also used for SharePoint 2013/2016 and also 2019 with a few modifications.

You can find the full script below where I will break down the script in a few sections to explain it in detail.

function get-SharePointLibraryDocuments{
#Variables
$rootSite = "https://spfiredeveloper.sharepoint.com"
$sourceSite = "https://spfiredeveloper.sharepoint.com/sites/dev/"
$sourceLibrary = "Documents"
$destinationPath = "D:\xxxxxxxxxx\DownloadLibrary\"

#Credentials
$userName = "mpadmin@spfiredeveloper.onmicrosoft.com"
$password = Read-Host "Please enter the password for $($userName)" -AsSecureString
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)

#set the client context
$context = New-Object Microsoft.SharePoint.Client.ClientContext($sourceSite)
$context.credentials = $Credentials

#Retrieve list
$list = $context.web.lists.getbytitle($sourceLibrary)
$context.load($list)
$context.executequery()

#Retrieve all items
$listItems = $list.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery()) 
$context.load($listItems)
$context.executequery()

foreach($listItem in $listItems) 
{ 
$fileName = $listItem["FileLeafRef"]
$fileUrl = $listItem["FileRef"]
$fileMeta = $listItem["Meta"]
$modified = $listItem["Modified"]
$created = $listItem["Created"]
$sourceItemPath = "$($rootSite)$($fileUrl)"
$destinationFolderPath = "$($destinationPath)$($fileMeta)\"
$destinationItemPath = "$($destinationFolderPath)$($fileName)"

try{
if((Test-Path $destinationFolderPath) -eq 0)
{
mkdir $destinationFolderPath | out-null
}

if((Test-Path $destinationItemPath) -eq 0)
{
try{
$client = New-Object System.Net.WebClient 
$client.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)
$client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")
$client.DownloadFile($sourceItemPath, $destinationItemPath)
$client.Dispose()

$file = Get-Item $destinationItemPath
$file.LastWriteTime = $modified
$file.CreationTime = $created

write-host "New document $($destinationItemPath)" -ForegroundColor green
}
catch{
write-host "Error occurred: $($destinationItemPath) - $($_.Exception.Message)" -foregroundcolor red
}
}
else{
try{
$item = get-childitem $destinationItemPath

if ($item.LastWriteTime.ToString("d-M-yyyy hh:mm:ss") -ne $modified.addhours(1).ToString("d-M-yyyy hh:mm:ss")){
$client = New-Object System.Net.WebClient 
$client.Credentials = $Credentials
$client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")
$client.DownloadFile($sourceItemPath, $destinationItemPath)
$client.Dispose()

$file = Get-Item $destinationItemPath
$file.LastWriteTime = $modified

write-host "Overwritten document $($destinationItemPath)" -ForegroundColor green
} 
else{
write-host "Skipped document $($destinationItemPath)" -ForegroundColor yellow
} 
}
catch{
write-host "Error occurred: $($destinationItemPath) - $($_.Exception.Message)" -foregroundcolor red
} 
}
}
catch{
write-host "Error occurred: $($_.Exception.Message)" -foregroundcolor red
}
} 
}

get-SharePointLibraryDocuments

Variables

I’ve hard coded the source and destination in this script as it was only required for one list but you can use a .CSV file if you have multiple sources and destinations. Note that you should keep the last slash or backslash in the string for $sourceSite and $destinationPath.

#Variables
$rootSite = "https://spfiredeveloper.sharepoint.com"
$sourceSite = "https://spfiredeveloper.sharepoint.com/sites/dev/"
$sourceLibrary = "Documents"
$destinationPath = "D:\xxxxxxxxxx\DownloadLibrary\"

Assembly

I’m running the script in the SharePoint Online Management Shell but you may need to add the SharePoint client DLL’s if you want to use this script on-premises.

SharePoint 2013

Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

SharePoint 2016/2019

Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

Credentials

The credentials are again hard coded for this blog but we created a secure text file with the username and password on the server which it will use for its daily schedule. The below can be used to test the script.

SharePoint Online

The below needs to be used for SharePoint Online CSOM

#Credentials
$userName = "mpadmin@spfiredeveloper.onmicrosoft.com"
$password = Read-Host "Please enter the password for $($userName)" -AsSecureString
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)

SharePoint On-Premise

The below needs to be used for SharePoint On-premises

#Credentials
$userName = "<Domain>\<LoginName>"
$password = Read-Host "Please enter the password for $($userName)" -AsSecureString
$Credentials = New-Object System.Net.NetworkCredential($userName, $password)

Client Context

Use the following two lines to get the client context

#get the client context
$context = New-Object Microsoft.SharePoint.Client.ClientContext($sourceSite)
$context.credentials = $Credentials

Retrieve the list

First we will retrieve the proper list

#Retrieve list
$list = $context.web.lists.getbytitle($sourceLibrary)
$context.load($list)
$context.executequery()

Retrieve all items

Next we will collect all items present in the list

#Retrieve all items
$listItems = $list.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery())  
$context.load($listItems)
$context.executequery()

File variables

We are going to use some item properties which we will store in variables directly after the for each. We will also build our source and destinations strings to be used in the next bits.

#file variables
$fileName = $listItem["FileLeafRef"]
$fileUrl = $listItem["FileRef"]
$fileMeta = $listItem["Meta"]
$modified = $listItem["Modified"]
$created = $listItem["Created"]
$sourceItemPath = "$($rootSite)$($fileUrl)"
$destinationFolderPath = "$($destinationPath)$($fileMeta)\"
$destinationItemPath = "$($destinationFolderPath)$($fileName)"

Create folder structure

We have a column “Meta” in our test environment which we use to categorize our items and we will need a folder for each unique “Meta”. The below section will create a folder if it doesn’t exist.

if((Test-Path $destinationFolderPath) -eq 0)
{
mkdir $destinationFolderPath | out-null
}

Download the file and save to the correct location

The below bit of code will first verify if the item does not exist and will then download the file using system.net.webclient which works for SharePoint on premises and SharePoint Online. We will update the last write time and creation time of the item to the same dates known in SharePoint. Otherwise it will have the dates of the download. You can also use the last saved date of the item but it is harder to retrieve this property and this property may have a mismatch with the modified date of SharePoint.

if((Test-Path $destinationItemPath) -eq 0)
{
try{
$client = New-Object System.Net.WebClient 
$client.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)
$client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")
$client.DownloadFile($sourceItemPath, $destinationItemPath)
$client.Dispose()</pre>
$file = Get-Item $destinationItemPath
$file.LastWriteTime = $modified
$file.CreationTime = $created

write-host "New document $($destinationItemPath)" -ForegroundColor green
}
catch{
write-host "Error occurred: $($destinationItemPath) - $($_.Exception.Message)" -foregroundcolor red
}
}

Overwrite items

In the next try catch we will verify if the item present is not the same item as currently in SharePoint because we will otherwise use server resources which are unnecessary as we only want to download new items or update current items. Note that I had to add “.addhours(1)” to my modified variable as in my case the time zone wasn’t matching for SharePoint and the file share. This may also be correct or off by 2 hours, but you will notice this when files are being overwritten and not skipped.

if ($item.LastWriteTime.ToString("d-M-yyyy hh:mm:ss") -ne $modified.addhours(1).ToString("d-M-yyyy hh:mm:ss")){
$client = New-Object System.Net.WebClient 
$client.Credentials = $Credentials
$client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")
$client.DownloadFile($sourceItemPath, $destinationItemPath)
$client.Dispose()</pre>
$file = Get-Item $destinationItemPath
$file.LastWriteTime = $modified

write-host "Overwritten document $($destinationItemPath)" -ForegroundColor green
}
else{
write-host "Skipped document $($destinationItemPath)" -ForegroundColor yellow
} 

Using the script

You can update all variables to represent your environment and copy/paste the code in the SharePoint Online Management Shell

Press enter to run the script and fill in your password

Press enter again and the script will create the necessary folders and download the items

You can run the script again to verify if the items are being skipped

Next I will be updating two items and run the script again

 

Configure SharePoint managed metadata columns using PowerShell CSOM

I received the request from a colleague to create a script that will configure all managed metadata columns in SharePoint Online to allow people to fill in values.
My colleague first tried this using PnP but was unable to configure the necessary property of a managed metadata column.
It is possible to set this property using the client object model (CSOM) in PowerShell.
You will need the SharePoint Online Client Components SDK which you can download at https://www.microsoft.com/en-us/download/confirmation.aspx?id=42038

We first verified that we were able to change this property by using the below script:

$url = "https://<TenantName>.sharepoint.com"
$userName = "admin@<TenantName>.onmicrosoft.com"
$password = Read-Host "Please enter the password for $($userName)" -AsSecureString

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Taxonomy.dll"

$SPOCredentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)

$context = New-Object Microsoft.SharePoint.Client.ClientContext($url)
$context.Credentials = $SPOcredentials
$web = $context.Web
$context.Load($web)
$list = $web.lists.GetByTitle("Documenten")
$context.Load($list)
$field = $list.fields.getbytitle("Zoekwoorden")

$context.load($field)
$context.ExecuteQuery()

$taxField = [Microsoft.SharePoint.Client.ClientContext].GetMethod("CastTo").MakeGenericMethod([Microsoft.SharePoint.Client.Taxonomy.TaxonomyField]).Invoke($context, $field)

$taxfield.open = $true
$taxfield.update()

$context.executequery()

Once we retrieved the correct field we had to cast this to a taxonomyField to be able to set the property “Open”.
The next step was to build a script around this that allows us to configure all managed metadata columns on the rootsite and all subsites.
This script has been upload to GitHub: https://github.com/peetersm12/ConfigureManagedMetadaColumnCSOM

The script will loop through all subsites to find the specified column on all the lists and libraries available.
You can first use a “preview” action to find all lists and libraries which have a column with the name you are looking to update.
Then you can update these columns so that users are allowed to fill in values.
Note that you can use this script for all updates for a specific column, just change the part where you  find the above code.

Using the script

Open the .ps1 file and copy the whole content of the file to preload the functions in the SharePoint Online Management Shell as administrator.

Next run 1 of the following commands:

To first list all lists/libraries which currently has the specified column title
set-mmcolumn -Action “Preview”

To update all lists and libraries which has the specified column title
set-mmcolumn -Action “Update”

Running the preview

Run the following command:

set-mmcolumn -Action "Preview"

and answer the following questions 1 by 1

The script will start and the output will be shown on screen, you can also verify the transcript although this isn’t color coded but search for “Found:”

We can see that our column has been found for the library “Documenten” and users are currently not allowed to fill in values.
Next run the script to update these columns.

Running the update

Run the following command:

set-mmcolumn -Action "Update"

and enter the following questions 1 by 1

The script will start and the output will be shown on screen, you can also verify the transcript although this isn’t color coded but search for “Found:”


We can see that our column has been found for the library “Documenten” and the value has been updated from False to true.
Run the preview again to see if all “green lines” are now set to true

PowerShell script to loop through each SharePoint Online site

This PowerShell script will let you loop through each SharePoint Online site to perform a specific action. I’ve been asked a couple of times to write a script to for example create a new document library in each site, add a web part or grant permissions. I always start with the below script so I only need to write the desired action and don’t need to worry about doing this for each site. Please let me know if you have used my script, for what purpose and if there are any questions.

Running the script

We will first start by opening the SharePoint Online Management Shell as administrator which can be downloaded at https://www.microsoft.com/en-us/download/details.aspx?id=35588.

 

 

 

 

You will only need to add the action below the start and above the comment section and it will run for each site. Just copy and paste the full script.

function Update-SPOWeb ($web){
#Get Web information and subsites
$context = New-Object Microsoft.SharePoint.Client.ClientContext($web)
$context.Credentials = $SPOCredentials
$web = $context.Web
$context.Load($web)
$context.Load($web.Webs)

#send the request containing all operations to the server
try{
$context.executeQuery()

#check any subweb if present
if ($web.Webs.Count -ne 0){
foreach ($subweb in $web.Webs){
Update-SPOWeb($subweb.url)
}
}

##########################
###                    ###
###    start action    ###
###                    ###
##########################

write-host $web.title

##########################
###                    ###
###    stop action     ###
###                    ###
##########################

}
catch{
write-host "Error: $($_.Exception.Message)" -foregroundcolor green
}
}

function Update-SPOWebs{
#variables that will be asked during the script
$adminUrl = Read-Host "Please enter the Office365 Admin URL (e.g. https://spfire-admin.sharepoint.com)"
$webUrl = Read-Host "Please enter the start url (e.g. https://spfire.sharepoint.com/sites/rootsite"
$userName = Read-Host "Please enter the Office365 admin account (e.g. mpadmin@spfire.onmicrosoft.com)"
$password = Read-Host "Please enter the password for $($userName)" -AsSecureString

# set SharePoint Online and Office 365 credentials
$SPOCredentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $password)
$credentials = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $userName, $password

#connect to to Office 365
try{
Connect-SPOService -Url $adminUrl -Credential $credentials
write-host "Info: Connected succesfully to Office 365" -foregroundcolor green
}
catch{
write-host "Error: $($_.Exception.Message)" -foregroundcolor red
Break delete-SPOSites
} 
Update-SPOWeb($webUrl)
}
Update-SPOWebs

 

image

Press enter

image

Fill in the required information and press enter

image

In this case it has only listed the names for each site but you do whatever you need.