I recently attempted to create a reliable way to backup SharePoint Online document libraries locally. After wrestling with various approaches, I developed a PowerShell script that allows you to connect to any SharePoint site, browse available libraries, and selectively download entire libraries with their folder structures intact. The script includes progress tracking and automatic retry logic for network interruptions.
Warnings
Well, yes, this does back up files that may be critical but if the service is not available remember to ask yourself how are you going to access these files in the event of a failure of more than just SharePoint - especially if those files are going to be backed up to OneDrive or other cloud dependent services
Remembering that OneDrive is part of SharePoint behind the scenes
If you would like to go down that rabbit hole, I’ve done an article about that exact situation based off this script on the link here
Prerequisites
Before diving into the script, you'll need PowerShell 5.x or later and the SharePoint PnP PowerShell module. Here's how to set up your environment:
Remove Old PnP Module (if installed as admin)
Uninstall-Module PnP.PowerShell -AllVersions -Force -ErrorAction SilentlyContinue
Install SharePoint PnP Module
Install-Module -Name SharePointPnPPowerShellOnline -Force -Scope CurrentUser
You'll also need appropriate permissions to access the SharePoint sites you want to backup.
The Script Breakdown
Initial Setup and Connection
The script starts by accepting a mandatory site name parameter and establishing a connection:
param([Parameter(Mandatory=$true)][string]$SiteName)
# Connection to SharePoint
$SiteURL = "https://<tenant>.sharepoint.com/sites/$SiteName"
Connect-PnPOnline -Url $SiteURL -UseWebLogin
I use -UseWebLogin to handle modern authentication, which works seamlessly with MFA and SSO configurations.
Discovering Document Libraries
Next, I enumerate all document libraries in the site, filtering out hidden system libraries:
Write-Host "`n--- DISCOVERING LIBRARIES IN $SiteName ---" -ForegroundColor Cyan
$Libraries = Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 -and $_.Hidden -eq $false }
The BaseTemplate value of 101 identifies document libraries specifically. I then build a numbered menu showing each library and its item count:
$LibraryMap = @{}
$i = 1
foreach ($Lib in $Libraries) {
$count = $Lib.ItemCount
Write-Host "[$i] $($Lib.Title) (Items: $count)" -ForegroundColor Yellow
$LibraryMap.Add($i, $Lib.Title)
$i++
}
User Selection and Validation
The script prompts for library selection and validates the input:
$selection = Read-Host "Enter the [Number] of the library you want to download"
$SelectedLibraryTitle = $LibraryMap[[int]$selection]
if (!$SelectedLibraryTitle) {
Write-Error "Invalid selection."
return
}
File Counting for Progress Tracking
Before downloading, I count all files recursively to enable accurate progress reporting:
# Initialize counters for progress tracking
$script:totalFiles = 0
$script:currentFile = 0
$script:failedFiles = @()
Function Count-Files ($FolderUrl) {
$items = Get-PnPFolderItem -FolderSiteRelativeUrl $FolderUrl -ErrorAction SilentlyContinue
foreach ($item in $items) {
$objType = $item.TypedObject.ToString()
if ($objType -like "*File*") {
$script:totalFiles++
}
elseif ($item.Name -ne "Forms" -and $objType -like "*Folder*") {
Count-Files -FolderUrl "$FolderUrl/$($item.Name)"
}
}
}
Recursive Download Function
The heart of the script is a recursive function that traverses the library structure with added resilience:
Function Download-Folder ($FolderUrl, $LocalPath) {
if (!(Test-Path $LocalPath)) {
New-Item $LocalPath -ItemType Directory -Force | Out-Null
}
try {
$items = Get-PnPFolderItem -FolderSiteRelativeUrl $FolderUrl -ErrorAction Stop
} catch {
Write-Warning "Failed to access folder: $FolderUrl - $_"
return
}
foreach ($item in $items) {
$objType = $item.TypedObject.ToString()
if ($objType -like "*File*") {
$script:currentFile++
$percentComplete = [math]::Round(($script:currentFile / $script:totalFiles) * 100, 2)
Write-Progress -Activity "Downloading from $SelectedLibraryTitle" `
-Status "File $script:currentFile of $script:totalFiles - $($item.Name)" `
-PercentComplete $percentComplete
# Try download with retry logic
$retryCount = 0
$maxRetries = 3
$downloaded = $false
while ($retryCount -lt $maxRetries -and -not $downloaded) {
try {
Get-PnPFile -Url $item.ServerRelativeUrl -Path $LocalPath -AsFile -Force -ErrorAction Stop
Write-Host " -> Downloaded: $($item.Name)" -ForegroundColor Gray
$downloaded = $true
} catch {
$retryCount++
if ($retryCount -eq $maxRetries) {
Write-Warning "Failed to download after $maxRetries attempts: $($item.Name)"
Write-Warning "Error: $_"
$script:failedFiles += $item.ServerRelativeUrl
} else {
Write-Warning "Download failed, retrying ($retryCount/$maxRetries): $($item.Name)"
Start-Sleep -Seconds 2
}
}
}
}
elseif ($item.Name -ne "Forms" -and $objType -like "*Folder*") {
Download-Folder -FolderUrl "$FolderUrl/$($item.Name)" `
-LocalPath (Join-Path $LocalPath $item.Name)
}
}
}
Key enhancements in this function:
- Progress bar showing current file, total files, and percentage complete
- Retry logic with up to 3 attempts per file
- Error handling that logs failures without stopping the entire backup
- 2-second delay between retry attempts for temporary network issues
Execution and Summary
The script creates a logical folder structure and provides comprehensive feedback:
# Count files first
Write-Host "`nCounting files in library..." -ForegroundColor Cyan
Count-Files -FolderUrl $InternalName
Write-Host "Total files to download: $script:totalFiles" -ForegroundColor Yellow
# Run the download
$DestPath = "C:\Quarantine\sharepoint-site-backup\Site\$SiteName\$InternalName"
Write-Host "Starting backup to $DestPath..." -ForegroundColor Cyan
Download-Folder -FolderUrl $InternalName -LocalPath $DestPath
# Clear progress bar
Write-Progress -Activity "Downloading from $SelectedLibraryTitle" -Completed
# Summary
Write-Host "`n--- DOWNLOAD SUMMARY ---" -ForegroundColor Cyan
Write-Host "Total files processed: $script:currentFile" -ForegroundColor Green
if ($script:failedFiles.Count -gt 0) {
Write-Host "Failed downloads: $($script:failedFiles.Count)" -ForegroundColor Red
Write-Host "`nFailed files:" -ForegroundColor Yellow
$script:failedFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
# Save failed files list for retry
$failedFilesPath = Join-Path $DestPath "failed_downloads.txt"
$script:failedFiles | Out-File $failedFilesPath
Write-Host "`nFailed files list saved to: $failedFilesPath" -ForegroundColor Yellow
} else {
Write-Host "`nSUCCESS: All files downloaded successfully!" -BackgroundColor DarkGreen -ForegroundColor White
}