#____________________________________________________________________________________________________________________________
#==================================================================================================
# OSI - Retention Exception Update + PHL Cleanup + Recycle Bin Cleanup (PnP)
# Single-Shot Mode (≤100 sites) — OPTIMIZED with PnP Batching
#==================================================================================================
#------------------------------ CONFIG -------------------------------------
$PolicyName = "Document Retention Policy"
$ExceptionsCsvPath = "E:\Reports\RP_Sites_2.csv"
$SitesCsvPath = "E:\Reports\Sites_2.csv"
$IPPSSessionUPN = "admin@spadmins.onmicrosoft.com"
$PnPClientId = "12a34567-f123-4567-891e-2aaf34567890"
$PHLPageSize = 2000
$BatchChunkSize = 100 # ← items per PnP batch (100 is the sweet spot)
$EnableRecycleBinCleanup = $true
$RecycleBinRowLimit = 0
#------------------------------ SECTION 1: RETENTION POLICY EXCEPTIONS ---------------------------
Write-Host "=== Updating Retention Policy Exceptions (Single-Shot) ===" -ForegroundColor Cyan
[array]$excludeSites = Import-Csv -Path $ExceptionsCsvPath |
Select-Object -ExpandProperty URL |
ForEach-Object { $_.Trim().TrimEnd("/") } |
Where-Object { $_ } |
Sort-Object -Unique
Write-Host "Loaded $($excludeSites.Count) exception site URLs from $ExceptionsCsvPath"
if ($excludeSites.Count -gt 100) {
Write-Host "ERROR: $($excludeSites.Count) sites exceed the 100-site static scoping limit." -ForegroundColor Red
Write-Host "Use batched mode with deployment wait, or switch to Adaptive Scopes." -ForegroundColor Red
return
}
Connect-IPPSSession -UserPrincipalName $IPPSSessionUPN
try {
Set-RetentionCompliancePolicy `
-Identity $PolicyName `
-AddSharePointLocationException $excludeSites
Write-Host "Successfully added all $($excludeSites.Count) exception sites in one call." -ForegroundColor Green
}
catch {
Write-Host "FAILED to add exceptions: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "Retention exception update completed." -ForegroundColor Cyan
#------------------------------ SECTION 2: PHL CLEANUP + RECYCLE BIN CLEANUP ---------------------
Write-Host "`n=== PHL Cleanup + Recycle Bin Cleanup ===" -ForegroundColor Cyan
$sites = Import-Csv -Path $SitesCsvPath
Write-Host "Loaded $($sites.Count) sites from $SitesCsvPath"
$hasClearRecycleCmd = $null -ne (Get-Command Clear-PnPRecycleBinItem -ErrorAction SilentlyContinue)
foreach ($site in $sites) {
$siteUrl = $site.URL.Trim().TrimEnd("/")
if (-not $siteUrl) { continue }
Write-Host "`nProcessing site: $siteUrl" -ForegroundColor Yellow
try {
Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $PnPClientId -ErrorAction Stop
}
catch {
Write-Host "Failed to connect to $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
continue
}
# ── PHL Cleanup ──────────────────────────────────────────────────────
$phl = Get-PnPList -Identity "Preservation Hold Library" -ErrorAction SilentlyContinue
if (-not $phl) {
Write-Host "No Preservation Hold Library found at $siteUrl" -ForegroundColor DarkGray
}
else {
Write-Host "Preservation Hold Library found at $siteUrl" -ForegroundColor Green
$items = @()
try {
$items = Get-PnPListItem -List "Preservation Hold Library" -PageSize $PHLPageSize -ScriptBlock {
param($pagedItems)
$pagedItems.Context.ExecuteQuery()
}
}
catch {
Write-Host "Failed to list PHL items at $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
$items = @()
}
if ($items.Count -gt 0) {
Write-Host "$($items.Count) items found in Preservation Hold Library — deleting in batches of $BatchChunkSize..." -ForegroundColor Cyan
# ╔══════════════════════════════════════════════════════════════╗
# ║ OPTIMIZATION: PnP Batching — replaces item-by-item delete ║
# ║ Groups up to $BatchChunkSize requests into ONE API call ║
# ╚══════════════════════════════════════════════════════════════╝
$totalDeleted = 0
for ($i = 0; $i -lt $items.Count; $i += $BatchChunkSize) {
$batch = New-PnPBatch
$end = [Math]::Min($i + $BatchChunkSize - 1, $items.Count - 1)
foreach ($item in $items[$i..$end]) {
Remove-PnPListItem -List "Preservation Hold Library" -Identity $item.Id -Batch $batch
}
try {
Invoke-PnPBatch -Batch $batch -ErrorAction Stop
$totalDeleted += ($end - $i + 1)
Write-Host " Batch deleted items $($i+1)–$($end+1) of $($items.Count)" -ForegroundColor DarkCyan
}
catch {
Write-Host " Batch failed at items $($i+1)–$($end+1): $($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
Write-Host "PHL cleanup complete: $totalDeleted / $($items.Count) items deleted" -ForegroundColor Green
}
else {
Write-Host "No items found in Preservation Hold Library" -ForegroundColor DarkGray
}
}
# ── Recycle Bin Cleanup ──────────────────────────────────────────────
if ($EnableRecycleBinCleanup) {
if (-not $hasClearRecycleCmd) {
Write-Host "Clear-PnPRecycleBinItem cmdlet not found. Per PnP docs, it may require PnP.PowerShell Nightly. Skipping recycle bin cleanup." -ForegroundColor DarkYellow
}
else {
try {
if ($RecycleBinRowLimit -gt 0) {
Clear-PnPRecycleBinItem -All -Force -RowLimit $RecycleBinRowLimit
}
else {
Clear-PnPRecycleBinItem -All -Force
}
Write-Host "Recycle bins cleared for: $siteUrl" -ForegroundColor Green
}
catch {
Write-Host "Failed to clear recycle bins for $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
}
}
}
}
Write-Host "`n=== DONE ===" -ForegroundColor Cyan