refactor(scripts): optimize system remediation and M365 export scripts
- 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
This commit is contained in:
1 parent
4fd95b76d6
commit
5ef82842f2
19 files changed
+1651
-1268
No files matched your search
Executable
+210
@@ -0,0 +1,210 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user