# 4) Helper to build a safe clickable URL from a server-relative path
function To-ClickableUrl([string]$tenantRoot, [string]$serverRelative) {
$segments = $serverRelative.TrimStart("/") -split "/"
$escaped = $segments | ForEach-Object { [Uri]::EscapeDataString($_) }
return $tenantRoot.TrimEnd("/") + "/" + ($escaped -join "/")
}
function Csv([object]$v){ if($null -eq $v){return ""}; $s=[string]$v; if($s -match '[,"\r\n]'){ $s=$s -replace '"','""'; return '"' + $s + '"' } $s }
# 5) Stream the CSV (header first)
$sw = New-Object System.IO.StreamWriter($OutCsv, $false, [Text.Encoding]::UTF8)
$sw.WriteLine("ClientRecordId,Environment,EntityType,TargetRootFolderPath,TotalTargetFiles,TargetRootFolderLink,ExtractedAtUtc,CountMethod")
foreach ($lib in $Libraries) {
$list = Get-PnPList -Identity $lib -Includes RootFolder
if (-not $list) { Write-Warning "Library '$lib' not found. Skipping."; continue }
$libRoot = $list.RootFolder.ServerRelativeUrl.TrimEnd("/")
# We'll count files by top-level folder under this library.
$counts = @{} # key: clientId; value: count
$paths = @{} # key: clientId; value: server-relative path to the top-level folder
Get-PnPListItem -List $lib -PageSize 4000 -Query $camlFilesRecursive -ScriptBlock {
param($items)
foreach ($it in $items) {
$dir = [string]$it["FileDirRef"]
if ($dir -notlike "$using:libRoot/*") { continue }
# Extract the segment right after the library name = ClientRecordId
# Split on "/", find the library segment, take next segment as candidate
$segments = $dir.TrimStart("/") -split "/"
$libIdx = [Array]::FindIndex($segments, [Predicate[string]]{ param($s) $s -ieq $using:lib })
if ($libIdx -lt 0 -or $libIdx + 1 -ge $segments.Length) { continue }
$candidate = $segments[$libIdx + 1]
# Enforce the ClientRecordId rule (digits only by default)
if ($candidate -notmatch '^\d+$') { continue }
# Count one file towards that clientId
if (-not $using:counts.ContainsKey($candidate)) {
$using:counts[$candidate] = 0
$using:paths[$candidate] = "$using:libRoot/$candidate"
}
$using:counts[$candidate]++
}
} | Out-Null
# Emit rows: one per client root folder
foreach ($clientId in $counts.Keys | Sort-Object) {
$rootPath = $paths[$clientId]
$link = To-ClickableUrl $tenantRoot $rootPath
$line = @(
Csv $clientId,
Csv "Online",
Csv $lib,
Csv $rootPath,
Csv $counts[$clientId],
Csv $link,
Csv $extractedAt,
Csv "Recursive(FILE scan, grouped by top-level folder)"
) -join ","
$sw.WriteLine($line)
}
}
$sw.Flush(); $sw.Dispose()
Write-Host "Recursive counts exported to: $OutCsv" -ForegroundColor Green