<# .SYNOPSIS Recursively downloads all SharePoint document libraries and files from a site using Microsoft Graph. .DESCRIPTION Connects to the Microsoft Graph API, resolves a SharePoint site URL, enumerates all document libraries (drives), and recursively downloads folders and files to a designated local target directory. Includes automatic filename sanitization for Windows file systems, pagination support, skipping existing files, and a final download statistics report. .PARAMETER SiteUrl The full URL of the target SharePoint site (e.g., "https://zefiromethane.sharepoint.com/sites/contentTypeHub"). .PARAMETER Destination The local root folder path where document libraries and files will be saved. .PARAMETER SkipAuthentication Switch parameter to skip 'Connect-MgGraph' if an active Graph session is already established. .EXAMPLE .\Export-SharePointSiteFiles.ps1 -SiteUrl "https://zefiromethane.sharepoint.com/sites/contentTypeHub" -Destination "F:\TeamSite" .EXAMPLE .\Export-SharePointSiteFiles.ps1 -SiteUrl "https://contoso.sharepoint.com/sites/hr" -Destination "C:\Backup\HR" -Verbose .NOTES Created: 2025-03-05 Modified: 2026-10-04 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [string]$SiteUrl, [Parameter(Mandatory = $true, Position = 1)] [ValidateNotNullOrEmpty()] [string]$Destination, [switch]$SkipAuthentication ) # 1. Prerequisite Module Check if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) { Write-Error "The 'Microsoft.Graph.Authentication' module is required. Install it using: Install-Module Microsoft.Graph" return } # 2. State & Metric Counters $script:downloadCount = 0 $script:skippedCount = 0 $script:folderCount = 0 $script:errorCount = 0 $script:totalBytes = 0 # 3. Helper Functions function Sanitize-PathSegment { param([string]$Name) # Replaces Windows reserved filename characters: \ / : * ? " < > | $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() $sanitized = $Name foreach ($char in $invalidChars) { $sanitized = $sanitized.Replace([string]$char, '_') } return $sanitized.TrimEnd('.') } function Get-GraphCollection { param([string]$Uri) do { $response = Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop foreach ($item in @($response.value)) { Write-Output $item } $Uri = $response.'@odata.nextLink' } while ($Uri) } function Copy-GraphFolder { param( [string]$DriveId, [string]$ChildrenUri, [string]$LocalFolder ) if (-not (Test-Path -LiteralPath $LocalFolder)) { New-Item -ItemType Directory -Path $LocalFolder -Force | Out-Null } try { $items = Get-GraphCollection -Uri $ChildrenUri } catch { Write-Error "Failed to retrieve folder contents from URI: $ChildrenUri. Error: $_" $script:errorCount++ return } foreach ($item in $items) { # Sanitize item name to prevent Windows file path errors $safeName = Sanitize-PathSegment -Name $item.name $localPath = Join-Path -Path $LocalFolder -ChildPath $safeName $escapedDriveId = [Uri]::EscapeDataString($DriveId) $escapedItemId = [Uri]::EscapeDataString([string]$item.id) # Handle Folders / Packages if ($null -ne $item.folder -or $null -ne $item.package) { $script:folderCount++ New-Item -ItemType Directory -Path $localPath -Force | Out-Null $childUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/items/$escapedItemId/children" Copy-GraphFolder -DriveId $DriveId -ChildrenUri $childUri -LocalFolder $localPath } # Handle Files elseif ($null -ne $item.file) { if (Test-Path -LiteralPath $localPath) { Write-Verbose "Skipping existing file: $localPath" $script:skippedCount++ continue } Write-Host "Downloading: $safeName" -ForegroundColor Cyan $contentUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/items/$escapedItemId/content" try { Invoke-MgGraphRequest -Method GET -Uri $contentUri -OutputFilePath $localPath -ErrorAction Stop $script:downloadCount++ if ($item.size) { $script:totalBytes += $item.size } } catch { Write-Error "Failed to download '$($item.name)' to '$localPath'. Error: $_" $script:errorCount++ } } else { Write-Warning "Unsupported item skipped: $($item.name)" $script:skippedCount++ } } } # 4. Main Execution Flow try { # Establish Graph Connection if (-not $SkipAuthentication) { Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow Connect-MgGraph -Scopes "Sites.Read.All" -NoWelcome } # Resolve SharePoint Site ID $parsedSiteUrl = [Uri]$SiteUrl $sitePath = $parsedSiteUrl.AbsolutePath.TrimEnd("/") if ([string]::IsNullOrWhiteSpace($sitePath)) { $siteLookupUri = "https://graph.microsoft.com/v1.0/sites/$($parsedSiteUrl.Host)" } else { $siteLookupUri = "https://graph.microsoft.com/v1.0/sites/$($parsedSiteUrl.Host):$sitePath" } Write-Host "Resolving SharePoint site..." -ForegroundColor Yellow $site = Invoke-MgGraphRequest -Method GET -Uri $siteLookupUri -ErrorAction Stop # Ensure root destination folder exists if (-not (Test-Path -LiteralPath $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null } # Enumerate Site Drives (Document Libraries) $drivesUri = "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives" $drives = Get-GraphCollection -Uri $drivesUri foreach ($drive in $drives) { $safeDriveName = Sanitize-PathSegment -Name $drive.name $localLibrary = Join-Path -Path $Destination -ChildPath $safeDriveName $escapedDriveId = [Uri]::EscapeDataString([string]$drive.id) Write-Host "`n========================================" -ForegroundColor Green Write-Host "Downloading Library: $($drive.name)" -ForegroundColor Green Write-Host "Local Target : $localLibrary" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green $rootChildrenUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/root/children" Copy-GraphFolder -DriveId $drive.id -ChildrenUri $rootChildrenUri -LocalFolder$localLibrary } # Display Execution Summary $MBTransferred = [math]::Round($script:totalBytes / 1MB, 2) Write-Host "`nSharePoint Download Summary:" -ForegroundColor Green Write-Host " Files Downloaded : $script:downloadCount ($MBTransferred MB)" Write-Host " Files Skipped : $script:skippedCount" Write-Host " Folders Created : $script:folderCount" Write-Host " Errors Encountered: $script:errorCount" } finally { if (-not $SkipAuthentication) { Write-Host "`nDisconnecting Microsoft Graph session..." -ForegroundColor Yellow Disconnect-MgGraph } }