# SharePoint Online Document Library Migration Script
# This script migrates a document library from one SharePoint site to another
# while preserving folder structure, metadata, and permissions
#Requires -Modules PnP.PowerShell
param(
[Parameter(Mandatory=$true)]
[string]$SourceSiteUrl,
[Parameter(Mandatory=$true)]
[string]$DestinationSiteUrl,
[Parameter(Mandatory=$true)]
[string]$SourceLibraryName,
[Parameter(Mandatory=$true)]
[string]$DestinationLibraryName,
[Parameter(Mandatory=$false)]
[switch]$MigratePermissions = $true,
[Parameter(Mandatory=$false)]
[switch]$MigrateVersions = $false,
[Parameter(Mandatory=$false)]
[string]$LogPath = ".\SharePointMigration.log"
)
# Function to write log messages
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
Write-Host $logMessage
Add-Content -Path $LogPath -Value $logMessage
}
# Function to create folder structure recursively
function Create-FolderStructure {
param(
[Microsoft.SharePoint.Client.Folder]$SourceFolder,
[string]$DestinationPath,
[Microsoft.SharePoint.Client.ClientContext]$DestContext
)
try {
# Get subfolders from source
$subFolders = Get-PnPFolderItem -FolderSiteRelativeUrl
$SourceFolder.ServerRelativeUrl -ItemType Folder
foreach ($subFolder in $subFolders) {
$newFolderPath = "$DestinationPath/$($subFolder.Name)"
Write-Log "Creating folder: $newFolderPath"
# Create folder in destination
try {
Add-PnPFolder -Name $subFolder.Name -Folder $DestinationPath -
ErrorAction SilentlyContinue
Write-Log "Successfully created folder: $($subFolder.Name)"
# Recursively create subfolders
Create-FolderStructure -SourceFolder $subFolder -DestinationPath
$newFolderPath -DestContext $DestContext
}
catch {
Write-Log "Error creating folder $($subFolder.Name): $
($_.Exception.Message)" -Level "ERROR"
}
}
}
catch {
Write-Log "Error in Create-FolderStructure: $($_.Exception.Message)" -Level
"ERROR"
}
}
# Function to copy files with metadata
function Copy-FilesWithMetadata {
param(
[string]$SourceFolderPath,
[string]$DestinationFolderPath
)
try {
# Get files from source folder
$files = Get-PnPFolderItem -FolderSiteRelativeUrl $SourceFolderPath -
ItemType File
foreach ($file in $files) {
Write-Log "Copying file: $($file.Name)"
try {
# Download file from source
$tempFile = [System.IO.Path]::GetTempFileName()
Get-PnPFile -Url $file.ServerRelativeUrl -Path (Split-Path
$tempFile) -Filename (Split-Path $tempFile -Leaf) -AsFile
# Connect to destination to upload
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
# Upload file to destination
$uploadResult = Add-PnPFile -Path $tempFile -Folder
$DestinationFolderPath -NewFileName $file.Name
# Get source file metadata
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
$sourceFile = Get-PnPFile -Url $file.ServerRelativeUrl -AsListItem
# Copy metadata to destination file
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
$destFile = Get-PnPFile -Url $uploadResult.ServerRelativeUrl -
AsListItem
# Copy field values (excluding system fields)
$fieldsToSkip = @("ID", "Created", "Modified", "Author", "Editor",
"Version", "_UIVersion", "_UIVersionString", "Attachments", "GUID",
"WorkflowVersion", "ParentVersionString", "ParentLeafName", "DocIcon", "ServerUrl",
"EncodedAbsUrl", "BaseName", "MetaInfo", "Level", "IsCurrentVersion",
"ItemChildCount", "FolderChildCount", "SMTotalSize", "SMLastModifiedDate",
"SMTotalFileStreamSize", "SMTotalFileCount", "File_x0020_Type",
"HTML_x0020_File_x0020_Type", "Edit", "LinkTitleNoMenu", "LinkTitle",
"SelectTitle", "InstanceID", "Order", "FileRef", "FileDirRef",
"Last_x0020_Modified", "Created_x0020_Date", "File_x0020_Size", "FSObjType",
"SortBehavior", "PermMask", "PrincipalCount", "CheckedOutUserId", "UniqueId",
"SyncClientId", "ProgId", "ScopeId", "VirusStatus", "CheckedOutTitle",
"_CheckinComment", "LinkCheckedOutTitle", "Modified_x0020_By", "Created_x0020_By",
"owshiddenversion", "_Level", "_IsCurrentVersion", "ContentTypeId", "Title",
"ContentType")
foreach ($field in $sourceFile.FieldValues.Keys) {
if ($fieldsToSkip -notcontains $field -and $sourceFile[$field]
-ne $null) {
try {
Set-PnPListItem -List $DestinationLibraryName -Identity
$destFile.Id -Values @{$field = $sourceFile[$field]}
}
catch {
Write-Log "Could not copy field $field : $
($_.Exception.Message)" -Level "WARNING"
}
}
}
# Clean up temp file
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
Write-Log "Successfully copied file: $($file.Name)"
}
catch {
Write-Log "Error copying file $($file.Name): $
($_.Exception.Message)" -Level "ERROR"
}
}
}
catch {
Write-Log "Error in Copy-FilesWithMetadata: $($_.Exception.Message)" -Level
"ERROR"
}
}
# Function to copy permissions
function Copy-Permissions {
param(
[string]$SourceItemPath,
[string]$DestinationItemPath,
[string]$ItemType = "File" # File or Folder
)
if (-not $MigratePermissions) {
return
}
try {
Write-Log "Copying permissions for $ItemType : $SourceItemPath"
# Connect to source to get permissions
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
if ($ItemType -eq "File") {
$sourceItem = Get-PnPFile -Url $SourceItemPath -AsListItem
} else {
$sourceItem = Get-PnPFolder -Url $SourceItemPath -Includes
ListItemAllFields
$sourceItem = $sourceItem.ListItemAllFields
}
# Check if item has unique permissions
if ($sourceItem.HasUniqueRoleAssignments) {
$roleAssignments = Get-PnPProperty -ClientObject $sourceItem -Property
RoleAssignments
# Connect to destination
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
if ($ItemType -eq "File") {
$destItem = Get-PnPFile -Url $DestinationItemPath -AsListItem
} else {
$destItem = Get-PnPFolder -Url $DestinationItemPath -Includes
ListItemAllFields
$destItem = $destItem.ListItemAllFields
}
# Break inheritance on destination item
Set-PnPListItemPermission -List $DestinationLibraryName -Identity
$destItem.Id -InheritPermissions:$false
# Copy each role assignment
foreach ($roleAssignment in $roleAssignments) {
$principal = $roleAssignment.Member
$roleDefinitions = $roleAssignment.RoleDefinitionBindings
foreach ($roleDef in $roleDefinitions) {
try {
if ($principal.PrincipalType -eq "User") {
Set-PnPListItemPermission -List $DestinationLibraryName
-Identity $destItem.Id -User $principal.LoginName -AddRole $roleDef.Name
} else {
Set-PnPListItemPermission -List $DestinationLibraryName
-Identity $destItem.Id -Group $principal.LoginName -AddRole $roleDef.Name
}
Write-Log "Applied permission: $($roleDef.Name) to $
($principal.LoginName)"
}
catch {
Write-Log "Error applying permission $($roleDef.Name) to $
($principal.LoginName): $($_.Exception.Message)" -Level "WARNING"
}
}
}
}
}
catch {
Write-Log "Error copying permissions: $($_.Exception.Message)" -Level
"ERROR"
}
}
# Function to recursively process folders
function Process-Folder {
param(
[string]$SourceFolderPath,
[string]$DestinationFolderPath
)
try {
Write-Log "Processing folder: $SourceFolderPath"
# Connect to source
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
# Copy files in current folder
Copy-FilesWithMetadata -SourceFolderPath $SourceFolderPath -
DestinationFolderPath $DestinationFolderPath
# Copy folder permissions
Copy-Permissions -SourceItemPath $SourceFolderPath -DestinationItemPath
$DestinationFolderPath -ItemType "Folder"
# Get subfolders
$subFolders = Get-PnPFolderItem -FolderSiteRelativeUrl $SourceFolderPath -
ItemType Folder
foreach ($subFolder in $subFolders) {
$sourceSubFolderPath = $subFolder.ServerRelativeUrl
$destSubFolderPath = "$DestinationFolderPath/$($subFolder.Name)"
# Recursively process subfolder
Process-Folder -SourceFolderPath $sourceSubFolderPath -
DestinationFolderPath $destSubFolderPath
}
}
catch {
Write-Log "Error processing folder $SourceFolderPath : $
($_.Exception.Message)" -Level "ERROR"
}
}
# Main execution
try {
Write-Log "Starting SharePoint Library Migration"
Write-Log "Source: $SourceSiteUrl/$SourceLibraryName"
Write-Log "Destination: $DestinationSiteUrl/$DestinationLibraryName"
# Check if PnP PowerShell module is installed
if (-not (Get-Module -ListAvailable -Name PnP.PowerShell)) {
Write-Log "PnP.PowerShell module not found. Installing..." -Level "WARNING"
Install-Module -Name PnP.PowerShell -Force -AllowClobber
}
# Connect to source site
Write-Log "Connecting to source site..."
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
# Verify source library exists
$sourceLibrary = Get-PnPList -Identity $SourceLibraryName -ErrorAction
SilentlyContinue
if (-not $sourceLibrary) {
throw "Source library '$SourceLibraryName' not found in $SourceSiteUrl"
}
# Connect to destination site
Write-Log "Connecting to destination site..."
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
# Check if destination library exists, create if not
$destLibrary = Get-PnPList -Identity $DestinationLibraryName -ErrorAction
SilentlyContinue
if (-not $destLibrary) {
Write-Log "Creating destination library: $DestinationLibraryName"
New-PnPList -Title $DestinationLibraryName -Template DocumentLibrary
}
# Get source library root folder
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
$sourceRootFolder = Get-PnPList -Identity $SourceLibraryName | Get-PnPProperty
-Property RootFolder
# Create folder structure in destination
Write-Log "Creating folder structure..."
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
$destLibraryPath = $DestinationLibraryName
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
Create-FolderStructure -SourceFolder $sourceRootFolder -DestinationPath
$destLibraryPath -DestContext $null
# Process root folder and all contents
Write-Log "Starting content migration..."
$sourceFolderPath = $sourceRootFolder.ServerRelativeUrl
Process-Folder -SourceFolderPath $sourceFolderPath -DestinationFolderPath
$destLibraryPath
# Copy library-level permissions if specified
if ($MigratePermissions) {
Write-Log "Copying library-level permissions..."
try {
Connect-PnPOnline -Url $SourceSiteUrl -Interactive
$sourceLib = Get-PnPList -Identity $SourceLibraryName
if ($sourceLib.HasUniqueRoleAssignments) {
Connect-PnPOnline -Url $DestinationSiteUrl -Interactive
$destLib = Get-PnPList -Identity $DestinationLibraryName
# Break inheritance
Set-PnPListPermission -Identity $DestinationLibraryName -
InheritPermissions:$false
# Copy permissions (this would need additional logic similar to
item permissions)
Write-Log "Library permissions copied (basic implementation)"
}
}
catch {
Write-Log "Error copying library permissions: $($_.Exception.Message)"
-Level "WARNING"
}
}
Write-Log "Migration completed successfully!"
}
catch {
Write-Log "Migration failed: $($_.Exception.Message)" -Level "ERROR"
throw
}
# Usage Examples:
<#
# Basic migration
.\SharePointMigration.ps1 -SourceSiteUrl
"https://tenant.sharepoint.com/sites/source" -DestinationSiteUrl
"https://tenant.sharepoint.com/sites/destination" -SourceLibraryName "Documents" -
DestinationLibraryName "MigratedDocuments"
# Migration without permissions
.\SharePointMigration.ps1 -SourceSiteUrl
"https://tenant.sharepoint.com/sites/source" -DestinationSiteUrl
"https://tenant.sharepoint.com/sites/destination" -SourceLibraryName "Documents" -
DestinationLibraryName "MigratedDocuments" -MigratePermissions:$false
# Migration with custom log path
.\SharePointMigration.ps1 -SourceSiteUrl
"https://tenant.sharepoint.com/sites/source" -DestinationSiteUrl
"https://tenant.sharepoint.com/sites/destination" -SourceLibraryName "Documents" -
DestinationLibraryName "MigratedDocuments" -LogPath "C:\Logs\Migration.log"
#>