- teams: harden Classic Teams removal, protect New Teams, and clean HKU run keys - dotnet: add bootstrapper EXE uninstall support and remove orphaned 3.1.* folders - teams: add MSIX package targeting and version parsing for vulnerable New Teams builds - m365: improve Graph SDK export speed, reuse sessions, and add null-safe handling
210 lines
7.5 KiB
PowerShell
Executable File
210 lines
7.5 KiB
PowerShell
Executable File
<#
|
|
.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
|
|
}
|
|
} |