Remediation/SharePointAndOneDrive/SPO-LARGE-SITES.ps1

# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk.
<#
.SYNOPSIS
    Produces the list, order and volumes of the site copy, and the decisions file (procedure SPO-LARGE-SITES).
 
.DESCRIPTION
    The copy goes through a third-party tool: the script assumes no tool. It gives a CSV ready to use: order (largest to smallest), source
    site, target address (from CON-SITES), volume, running total and largest library.
    decisions\SPO-LARGE-SITES.csv says for each site: Copy, Split by library or Exclude.
    The sites excluded by SPO-INACTIVE are proposed as Exclude. The large site threshold comes from the LargeSiteGb setting.
    In an -Apply run, the script reads the real size of the sites on the source.
    Limit: splitting, incremental passes and measuring the last pass are done in the chosen tool.
#>

$c = Open-TcRemediation

$ThresholdGb = [double](Get-TcRemediationOption 'LargeSiteGb')
$TechnicalSites = '^(SPSMSITEHOST|SRCHCEN|APPCATALOG|POINTPUBLISHING|REDIRECTSITE|TEAMCHANNEL)'

function Read-OtherDecision {
    param([string]$Name)
    $f = $Name
    if (Test-TcDecisionFile $f) { return @(Read-TcDecisionRow $f) }
    return @()
}

$addresses = @{}
foreach ($l in Read-OtherDecision 'CON-SITES.csv') { $addresses[((Get-TcText $l 'Source site')).TrimEnd('/').ToLowerInvariant()] = (Get-TcMember $l 'Target address') }
$excludedItems = @{}
foreach ($l in Read-OtherDecision 'SPO-INACTIVE.csv') { if ((Get-TcMember $l 'Decision') -in 'Read only', 'Archive', 'Delete') { $excludedItems[((Get-TcText $l 'Site')).TrimEnd('/').ToLowerInvariant()] = 1 } }
if (-not $addresses.Count) { Write-TcRemediationAlert 'Target addresses missing: run CON-SITES first (BUILD-04)' }

$proposals = [System.Collections.Generic.List[object]]::new()
foreach ($t in $c.Tenants) {
    Write-TcRemediationTenant $t
    if (-not (Test-TcCollectionComplete $t 'sites')) { Write-TcRemediationAlert 'Incomplete site collection: the list may be partial' }
    $sites = @(Get-TcRemediationTable $t 'sites' | Where-Object { $_['Template'] -notmatch $TechnicalSites } | Sort-Object { - [double]$_['Storage MB'] })
    $large = @($sites | Where-Object { [double]$_['Storage MB'] / 1024 -ge $ThresholdGb })
    $volume = (Get-TcMember (@($sites | ForEach-Object { [double]$_['Storage MB'] }) | Measure-Object -Sum) 'Sum') / 1024
    Write-TcRemediationNote ("{0} site(s), {1:N1} GB, {2} of {3} GB or more" -f $sites.Count, $volume, $large.Count, $ThresholdGb)
    $libraries = @(Get-TcRemediationTable $t 'libraries' | Where-Object { $_['Size MB'] })
    $rank = 0
    $total = 0
    foreach ($s in $sites) {
        $rank++
        $key = ([string]$s['Url']).TrimEnd('/').ToLowerInvariant()
        $go = [math]::Round([double]$s['Storage MB'] / 1024, 1)
        $total += $go
        $b = $libraries | Where-Object { ([string]$_['Site']).TrimEnd('/').ToLowerInvariant() -eq $key } | Sort-Object { - [double]$_['Size MB'] } | Select-Object -First 1
        $isLarge = $go -ge $ThresholdGb
        $proposals.Add([ordered]@{ Tenant = $t; Order = $rank; Site = $s['Url']; 'Target address' = $addresses[$key]; 'Volume GB' = $go; 'Total GB' = [math]::Round($total, 1); 'Large site' = $(if ($isLarge) { 'Yes' } else { 'No' })
                                      'Largest library' = $(if ($b) { '{0} ({1:N0} Mo)' -f $b['List'], [double]$b['Size MB'] } else { '' }); 'Days inactive' = $s['Days inactive']
                                      Proposal = $(if ($excludedItems[$key]) { 'Exclude' } else { 'Copy' }) })
    }
}
$decisions = @(Get-TcDecision -Name 'SPO-LARGE-SITES' -Proposal @($proposals) -Key 'Tenant', 'Site' -Choice 'Copy', 'Split by library', 'Exclude')

$bySite = @{}
foreach ($d in $decisions) { $bySite[('{0}|{1}' -f (Get-TcMember $d 'Tenant'), (Get-TcMember $d 'Site'))] = (Get-TcMember $d 'Decision') }
foreach ($t in $c.Tenants) {
    $copyRows = @($proposals | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t } | ForEach-Object {
        $l = [pscustomobject]$_
        $l | Add-Member -NotePropertyName 'Decision' -NotePropertyValue ([string]$bySite[('{0}|{1}' -f $t, (Get-TcMember $_ 'Site'))]) -PassThru })
    Export-TcRemediationReport -Name "site-copy-plan-$t" -Rows $copyRows | Out-Null
}

Invoke-TcRemediationPlan -Verify {
    param($t)
    $thresholdMb = $ThresholdGb * 1024
    $realOnes = @(Invoke-TcServiceCommand -Tenant $t -Service Pnp -Command 'Get-PnPTenantSite' -Parameters @{ Detailed = $true } | Where-Object { (Get-TcMember $_ 'StorageUsageCurrent') -ge $thresholdMb })
    $plannedItems = @(Get-TcRemediationTable $t 'sites' | Where-Object { [double]$_['Storage MB'] -ge $thresholdMb })
    New-TcRemediationResult Info ("{0} large site(s) read on the source, {1} in the snapshot" -f $realOnes.Count, $plannedItems.Count)
    if ($realOnes.Count -ne $plannedItems.Count) { New-TcRemediationResult Warning 'The number of large sites changed: take a new snapshot before the pre-copy' }
}
Close-TcRemediation