Guides & Tools
SHARING IS CARING
Patterns & Practices
Collection of tools and guidance meant to help you extend Microsoft 365 to your needs following best practices.
Don’t reinvent the wheel — focus on what truly matters for your organization.
Learn. Reuse. Share.


Consider when Migrating SharePoint Site
https:/pnp.github.io/powershell/cmdlets/Copy-PnPFile.html
Move-PnPFolder | PnP PowerShell
https:/pnp.github.io/powershell/cmdlets/Move-PnPFolder.html
#Create a form to collect the site name, subsite name, sub-subsite name, and document library name
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Download the image
if($null -eq $imagePath){
$imageUrl = "https://cspowerapplicationsstor.blob.core.windows.net/repository/cleanLogo.png"
$imagePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "cleanLogo.png")
Invoke-WebRequest -Uri $imageUrl -OutFile $imagePath}
# Create the form
$form = New-Object System.Windows.Forms.Form
$form.Text = "Copy Site Data to sites/Archive"
$form.Size = New-Object System.Drawing.Size(300, 500)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$form.MaximizeBox = $false
$form.MinimizeBox = $false
# Create a label for site name
$labelForm = New-Object System.Windows.Forms.Label
$labelForm.Text = "Fill out approprate fields"
$labelForm.Size = New-Object System.Drawing.Size(280, 20)
$labelForm.Location = New-Object System.Drawing.Point(10, 60)
$labelForm.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm)
# Create a label for site name
$labelForm2 = New-Object System.Windows.Forms.Label
$labelForm2.Text = "Library Will Remain Until Renamed"
$labelForm2.Size = New-Object System.Drawing.Size(280, 20)
$labelForm2.Location = New-Object System.Drawing.Point(10, 90)
$labelForm2.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm2.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm2)
# Load and add the image
$image = [System.Drawing.Image]::FromFile($imagePath)
$pictureBox = New-Object System.Windows.Forms.PictureBox
$pictureBox.Image = $image
$pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
$pictureBox.Size = New-Object System.Drawing.Size(260, 40)
$pictureBox.Location = New-Object System.Drawing.Point(10, 10)
$form.Controls.Add($pictureBox)
# Create a label for site name
$labelSite = New-Object System.Windows.Forms.Label
$labelSite.Text = "Site Name:"
$labelSite.Size = New-Object System.Drawing.Size(280, 20)
$labelSite.Location = New-Object System.Drawing.Point(10, 120)
$labelSite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSite)
# Create a text box for site name
$textBoxSite = New-Object System.Windows.Forms.TextBox
$textBoxSite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSite.Location = New-Object System.Drawing.Point(10, 150)
$form.Controls.Add($textBoxSite)
# Create a label for subsite name
$labelSubsite = New-Object System.Windows.Forms.Label
$labelSubsite.Text = "Subsite Name (optional):"
$labelSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubsite.Location = New-Object System.Drawing.Point(10, 180)
$labelSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubsite)
# Create a text box for subsite name
$textBoxSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubsite.Location = New-Object System.Drawing.Point(10, 210)
$form.Controls.Add($textBoxSubsite)
# Create a label for sub-subsite name
$labelSubSubsite = New-Object System.Windows.Forms.Label
$labelSubSubsite.Text = "Sub-Subsite Name (optional):"
$labelSubSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubSubsite.Location = New-Object System.Drawing.Point(10, 240)
$labelSubSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubSubsite)
# Create a text box for sub-subsite name
$textBoxSubSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubSubsite.Location = New-Object System.Drawing.Point(10, 270)
$form.Controls.Add($textBoxSubSubsite)
# Create a label for document Library name
$labeldocLibrary = New-Object System.Windows.Forms.Label
$labeldocLibrary.Text = "Document Library: $documentLibrary"
$labeldocLibrary.Size = New-Object System.Drawing.Size(280, 20)
$labeldocLibrary.Location = New-Object System.Drawing.Point(10, 300)
$labeldocLibrary.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labeldocLibrary.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labeldoclibrary)
# Create a text box for document Library name
$textdocLibrary = New-Object System.Windows.Forms.TextBox
$textdocLibrary.Size = New-Object System.Drawing.Size(260, 20)
$textdocLibrary.Location = New-Object System.Drawing.Point(10, 330)
$textdocLibrary.Text = $documentLibrary # Set default value
$form.Controls.Add($textdocLibrary)
# Create a button
$button = New-Object System.Windows.Forms.Button
$button.Text = "OK"
$button.Size = New-Object System.Drawing.Size(75, 23)
$button.Location = New-Object System.Drawing.Point(100, 370)
$form.Controls.Add($button)
# Add event handler for text box text changed event
$textdocLibrary.Add_TextChanged({
if ([string]::IsNullOrWhiteSpace($textdocLibrary.Text)) {
$button.Enabled = $false # Enable the button if text box is not empty
} else {
$button.Enabled = $true # Disable the button if text box is empty
}
})
if ($null -eq $documentLibrary) {
$button.Enabled = $false # Disable the button initially
}
# Define the button click event
$button.Add_Click({
$form.Tag = @{
SubSitesiteName = $textBoxSubSubsite.Text
SiteName = $textBoxSite.Text
SubsiteName = $textBoxSubsite.Text
DocLibrary = $textdocLibrary.Text
}
$form.Close()
})
# Show the form
$form.ShowDialog()
# Output the collected data
$form.Tag
# Get the site and subsite names from the form
$siteName = $form.Tag.SiteName -replace '\s', ''
# Output the site name
Write-Host "Site Name entered: $siteName"
$subsiteName = $form.Tag.SubsiteName -replace '\s', ''
if ($form.Tag.SubsiteName) {
Write-Host "Site Name entered: $subsiteName"
}
$subSubsiteName = $form.Tag.SubSitesiteName -replace '\s', ''
if ($form.Tag.SubSitesiteName) {
Write-Host "Site Name entered: $subSubsiteName"
}
if ($form.Tag.DocLibrary) {
$documentLibrary = $form.Tag.DocLibrary -replace '\s', ''
}
else {
$documentLibrary = $documentLibrary
}
Write-Host "Document Library: $documentLibrary"
#Set Variables
$contentType = ""
$listsFolder = ""
$clinetId = "ac16ec23-e5e2-4fbb-8eda-108e5f760593"
$tenant = "CleanSlateCenter.onmicrosoft.com"
$thumbprint = "C53191C3422366FF6C5D38AB999ECDA3E549DF82"
$siteBaseUrl = "https://cleanslatecenter.sharepoint.com/"
$siteDestinationURL = "sites/Archive"
$siteMigrateUrl = $siteBaseUrl + $siteDestinationURL
$siteSourceBaseUrl = if ($subsiteName) {
if ($subSubsiteName) {
$siteName + "/" + $subsiteName + "/" + $subSubsiteName
} else {
$siteName + "/" + $subsiteName
}
} else {
$siteName
}
$subsite = $siteSourceBaseUrl -replace "/", "_"
#$siteSourceBaseUrl = $form.Tag.SiteName
$siteDestinationFolder = "$documentLibrary/$subsite"
$listsFolder = $siteDestinationFolder + "/Lists"
$doclistName = $siteDestinationFolder.Split('/')[0]
$siteDestinationRoot = $siteDestinationURL + "/" + $siteDestinationFolder -replace "/$subsite", ''
$siteDestinationFullPath = $siteDestinationURL + "/" + $siteDestinationFolder
$siteUrl = $siteBaseUrl + $siteSourceBaseUrl
$contentType = $subsite + "ContentTypeTemplate.xml"
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Check if the document library exists in the destination site
$libraryExists = Get-PnPList -Identity $documentLibrary -ErrorAction SilentlyContinue
if (-not $libraryExists) {
# Create the document library in the destination site
New-PnPList -Title $documentLibrary -Template DocumentLibrary
}
# Create the field Creators and Last Update by if it doesn't exist
# Check if the field exists
$field = Get-PnPField -List $doclistName -Identity "LastUpdateBy" -ErrorAction SilentlyContinue
if ($null -eq $field) {
Add-PnPField -List $doclistName -DisplayName "LastUpdateBy" -InternalName "LastUpdateBy" -Type Text
Write-Host "Field 'LastUpdateBy' created."
} else {
Write-Host "Field 'LastUpdateBy' already exists."
}
$field = Get-PnPField -List $doclistName -Identity "Creator" -ErrorAction SilentlyContinue
if ($null -eq $field) {
Add-PnPField -List $doclistName -DisplayName "Creator" -InternalName "Creator" -Type Text
Write-Host "Field 'Creator' created."
} else {
Write-Host "Field 'Creator' already exists."
}
# Check if the folder exists in the destination site
$folderExists = Get-PnPFolder -Url $siteDestinationFolder -ErrorAction SilentlyContinue
if (-not $folderExists) {
# Create the folder in the destination site
Add-PnPFolder -Name $subsite -Folder "/$siteDestinationRoot"
Add-PnPFolder -Name "Lists" -Folder "$siteDestinationFolder"
}
# Collect the folders and files from the source site
try {
# Connect to the source site
Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Retrieve all lists including the properties you'll need for filtering.
$lists = Get-PnPList -Includes Title, BaseTemplate, Hidden, RootFolder.ServerRelativeUrl
$documentLibraries = $lists | Where-Object {
$_.BaseTemplate -eq 101 -or
(-not $_.Hidden) -and ($_.Title -notmatch 'Health & Safety Documents') -and
($_.Title -notmatch 'Forms') -and ($_.Title -notmatch 'Workflow Tasks') -and
($_.RootFolder.ServerRelativeUrl -notmatch 'Lists')
}
# Initialize an array to store all folders
$folders = @()
$allfolders = @()
$docLibraries = @()
$folderfiles = @()
$topDocumentFolders = @()
$listsSections = @()
$files1stLevel = @()
$rootLists = @()
# Loop through each document library and get folders
foreach ($library in $documentLibraries) { #$library = $documentLibraries[1]
$libraryRootFolder = Get-PnPFolder -Url $library.RootFolder.ServerRelativeUrl
$RootCellar = $libraryRootFolder.Name #$library.Title
$docLibraries += $libraryRootFolder
# Get all top level folders in the document library
$docsRootDir = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType Folder
$docsRootDir = $docsRootDir | Where-Object { $_.Name -ne "Forms" }
$topDocumentFolders += $docsRootDir
# Get all top level files in the document library
$libraryItems = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File -Recursive
$libraryLevelFiles = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File
$siteFiles = $libraryItems | Where-Object {$libraryLevelFiles -notcontains $_}
# Acquire the properties of the files
foreach ($item in $libraryLevelFiles) {
$listItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
if ($null -ne $listItem.Id){ #($listItem -and $null -ne $listItem.Id) {
$folderfiles += $listItem
$files1stLevel += $listItem
}
}
foreach ($item in $siteFiles) {
$fileItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
if ($null -ne $fileItem.Id) {
$folderfiles += $fileItem
}
}
}
# Retrieve all lists and libraries
$lists = Get-PnPList
# Filter lists that are in the root directory
$rootLists = $lists | Where-Object {-not $_.Hidden -and $_.Title -notmatch "Tasks" -and $_.Title -notmatch "Documents"}
$listFolders = $rootLists | Where-Object {$_.RootFolder.ServerRelativeUrl -notmatch "Lists"}
$listsSections = $rootLists | Where-Object {$listFolders -notcontains $_}
# Extract the content types and lists from the source site
Get-PnPSiteTemplate -Out $contentType -Handlers ContentTypes -Force
}
catch {
# Handle the error
Write-Host "Error details: $_"
exit
}
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Apply the template to the destination site
# Invoke-PnPSiteTemplate -Path $contentType -Verbose
# Create the document librarry folders in the destination site
foreach ($library in $docLibraries) {
$RootCellar = $library.Name
$libraryLanding = $siteDestinationFolder + "/" + $RootCellar
# Check if the folder exists in the destination site
$folderExists = Get-PnPFolder -Url $libraryLanding -ErrorAction SilentlyContinue
if (-not $folderExists) {
# Create the folder in the destination site
Add-PnPFolder -Name $RootCellar -Folder $siteDestinationFolder
}
$allfolders += $library
}
# Copy each file to the root directories in destination site
foreach ($file in $files1stLevel) {
$fieldValues = $file.FieldValues
$title = $fieldValues["Title"]
#$pageName = $fieldValues["FileLeafRef"]
$SourcePageURL = $fieldValues["FileRef"]
$SourceUrl = $fieldValues["FileDirRef"]
$pageLanding = $SourceUrl -replace "^/$siteSourceBaseUrl", $siteDestinationFolder
$pagetoCopy = $SourcePageURL -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
# Check if the file exists in the destination site
$fileExists = Get-PnPFile -Url $pagetoCopy -ErrorAction SilentlyContinue
if ($fileExists) {
Write-Host "File already exists in the destination site." -ForegroundColor Yellow
continue
}
else {
Copy-PnPFile -SourceUrl $SourcePageURL -TargetUrl $pageLanding -Force
Write-Host "File copied successfully."
}
}
# Add the folder depth field values to the files
Function InitializeAndAddField {
param (
[array]$folders,
[string]$fieldName
)
$folders | ForEach-Object {
if (-not $_.PSObject.Properties[$fieldName]) {
$_ | Add-Member -MemberType NoteProperty -Name $fieldName -Value ($_.ServerRelativeUrl -split '/').Count
} else {
$_.$fieldName = ($_.ServerRelativeUrl -split '/').Count
}
}
return $folders
}
$topDocumentFolders = InitializeAndAddField -folders $topDocumentFolders -fieldName "FolderDepth"
$topDocumentFolders = $topDocumentFolders | Where-Object { $_.Name -ne "Forms" }
foreach($top in $topDocumentFolders){
Write-Host "Folder: $($top.ServerRelativeUrl) Depth: $($top.FolderDepth)"
}
# Function to recursively process folders and their subfolders
function Copy-FoldersRecursively {
param (
[array]$folders
)
foreach ($folder in $folders) {
# Process the current folder
$folderUrl = $folder.ServerRelativeUrl -replace "^/$siteDestinationFullPath", "/$siteSourceBaseUrl"
$lastSlashIndex = $folderUrl.LastIndexOf('/')
$folderPath = $folderUrl.Substring(0, $lastSlashIndex)
$itemCount = $folder.ItemCount
Write-Output "Processing folder: $folderUrl"
# Replace the source URL with the destination URL
$destinationUrl = $folderPath -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
$nextLevelFolder = $folderUrl -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
try {
if($itemCount -ne 0){
Copy-PnPFolder -SourceUrl $folderUrl -TargetUrl $destinationUrl -Force -Overwrite
Write-Host "Copied folder from $folderUrl to $destinationUrl" -ForegroundColor Green
} else {
# Create the empty folder at the destination
Add-PnPFolder -Name (Split-Path $folderUrl -Leaf) -Folder $destinationUrl -ErrorAction SilentlyContinue
Write-Host "Created empty folder at $destinationUrl" -ForegroundColor Yellow
}
} catch {
Write-Host "Error copying or creating folder from $folderUrl to $destinationUrl" -ForegroundColor Blue
Write-Host "Error details: $_" -ForegroundColor Magenta
}
Start-Sleep -Seconds 5
# Get subfolders of the current folder
$subfolders = Get-PnPFolderItem -FolderSiteRelativeUrl $nextLevelFolder -ItemType Folder -ErrorAction SilentlyContinue
if ($subfolders) {
# Log retrieved subfolders for debugging
Write-Output "Retrieved subfolders for $($folderUrl): $($subfolders | ForEach-Object { $_.ServerRelativeUrl })"
# Filter out subfolders named "Forms"
$filteredSubfolders = $subfolders | Where-Object { $_.Name -ne "Forms" }
# Recursively process subfolders
Copy-FoldersRecursively -folders $filteredSubfolders
} else {
Write-Output "No subfolders found for $folderUrl"
}
}
}
# Measure the execution time of the Copy-FoldersRecursively function
$executionTime = Measure-Command {
Copy-FoldersRecursively -folders $topDocumentFolders
}
# Display the elapsed time
Write-Host "The Copy-FoldersRecursively function took $($executionTime.TotalSeconds) seconds to complete."
# If needed, add a sleep based on the measured time
if ($executionTime.TotalSeconds -lt 5) {
Write-Host "Adding a sleep of 5 seconds to ensure all files are available for update."
Start-Sleep -Seconds 5
}
foreach ($item in $folderfiles) { #$item = $folderfiles[10]
$fieldValues = $null
$fieldValues = $item.FieldValues
$title = $fieldValues["Title"]
$itemId = $fieldValues["ID"]
if($null -ne $itemId) {
$userEditor = ""
$userCreator = ""
# Access metadata
$created = $fieldValues["Created"]
$modified = $fieldValues["Modified"]
$author = $fieldValues["Author"]
$editor = $fieldValues["Editor"]
$userCreated = $author.Email
$userEdited = $editor.Email
$fileName = $fieldValues["FileLeafRef"]
$fileUrl = $fieldValues["FileDirRef"]
$destinationUrlFile = $fileUrl -replace "^/$siteSourceBaseUrl", "/$siteDestinationFullPath"
#$archiveFolder = $siteDestinationFullPath -replace "/$subsite", ""
$DestinationFileName = $destinationUrlFile + "/" + $fileName
$NewFile = Get-PnPFile -Url $DestinationFileName -AsListItem
if ($null -ne $NewFile.Id) {
$values = @()
# Prepare the values to update
$values = @{
"Creator" = $userCreated
"Created" = $created
"LastUpdateBy" = $userEdited
"Modified" = $modified
}
$pnpUser = "i:0#.f|membership|" + $userEdited
$userEditor = Get-PnPUser -Identity $pnpUser -ErrorAction SilentlyContinue
if ($userEditor) {
$values["Editor"] = $userEdited
}
$pnpCreator = "i:0#.f|membership|" + $userCreated
$userCreator = Get-PnPUser -Identity $pnpCreator -ErrorAction SilentlyContinue
if ($userCreator) {
$values["Author"] = $userCreated
}
# Update the metadata on the new file with the modified and modified by source values
Write-Host "Updating metadata for file: $DestinationFileName with ID: $($NewFile.Id)"
Set-PnPListItem -List $doclistName -Identity $NewFile.Id -Values $values
}
}
}
# Copy the lists and libraries to the destination site
# Connect to the source site
Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
# Define the SharePoint list name
$listName = ""
$listXml = ""
$listName = $root.Title
$listXml = $subsite + $listName + ".xml"
#Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Extract the content types and lists from the source site
Get-PnPSiteTemplate -Out $listXml -ListsToExtract $listName -Handlers Lists -Force # $listName -Handlers Lists
Add-PnPDataRowsToSiteTemplate -Path $listXml -List $listName
}
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
# Define the SharePoint list name
$listName = ""
$listCreating = ""
$listUrl = ""
$listXml = ""
$listName = $root.Title
$listCreating = $subsite + "_" + $listName
$listUrl = $siteUrl + "/Lists/" + $listName
$listXml = $subsite + $listName + ".xml"
# Upload the template XML to the destination site
if($listXml -ne "") {
Add-PnPFile -Path $listXml -Folder $listsFolder
<#try {
Set-PnPList -Identity $listName -Title $listCreating
Write-Output "Successfully renamed list: $listName to $listCreating"
} catch {
Write-Output "Failed to rename list: $listName"
Write-Output "Error: $($_.Exception.Message)"
}#>
Remove-Item -Path $listXml -Force
}
}
Add-PnPFile -Path $contentType -Folder $listsFolder
Remove-Item -Path $contentType -Force
Guiding your business through the project
Experience the fusion of imagination and expertise with Études—the catalyst for architectural transformations that enrich the world around us.
Meet our team
Our comprehensive suite of professionals caters to a diverse team, ranging from seasoned architects to renowned engineers.
Francesca Piovani
Founder, CEO & Architect
Rhye Moore
Engineering Manager
Helga Steiner
Architect
Ivan Lawrence
Project Manager
We’ve worked with some of the best companies.
FAQs
Enhance your architectural journey with the Études Architect app.
- Collaborate with fellow architects.
- Showcase your projects.
- Experience the world of architecture.
