public/Clear-PSProfileSnapshotOrphans.ps1

<#
    .SYNOPSIS
        Removes orphaned snapshot files that lack their corresponding backup file.
    .DESCRIPTION
        Cleans up hash, metadata, and pre-restore safety backup files that no longer have
        an associated snapshot backup file. This can happen if backup files are manually
        deleted or due to incomplete removal operations.
        
        Pre-restore files (.backup.pre-restore) are safety backups created before restoration
        operations. Orphaned pre-restore files are those whose corresponding snapshot no longer exists.
    .INPUTS
        None
    .OUTPUTS
        None
    .EXAMPLE
        Clear-PSProfileSnapshotOrphans

        Removes all orphaned snapshot files (hash, metadata, and pre-restore backups).
    .NOTES
        This is a maintenance function and should be run periodically if manual deletions occur.
        Pre-restore files are automatically cleaned up if their corresponding snapshot backups no longer exist.
#>


function Clear-PSProfileSnapshotOrphans {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param()

    try {
        $profilesPath = Split-Path -Path $profile -Parent
        
        # Get all orphan candidate files
        $hashFiles = Get-ChildItem -Path $profilesPath -Filter "*.hash" -ErrorAction Stop
        $metadataFiles = Get-ChildItem -Path $profilesPath -Filter "*.metadata" -ErrorAction Stop
        $preRestoreFiles = Get-ChildItem -Path $profilesPath -Filter "*.backup.pre-restore" -ErrorAction Stop
        $backupFiles = Get-ChildItem -Path $profilesPath -Filter "*.backup" -ErrorAction Stop
        
        if (-not $hashFiles -and -not $metadataFiles -and -not $preRestoreFiles) {
            Write-Host "✅ No orphaned files found." -ForegroundColor Green
            return
        }

        # Extract all GUIDs from backup files for quick lookup (exclude pre-restore files)
        $backupGUIDs = @{}
        foreach ($backupFile in $backupFiles) {
            # Skip pre-restore files when building the backup GUID list
            if ($backupFile.Name -notlike "*.backup.pre-restore") {
                $guid = $backupFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                $backupGUIDs[$guid] = $true
            }
        }

        # Find orphaned hash files
        $orphanedHashes = @()
        foreach ($hashFile in $hashFiles) {
            $guid = $hashFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
            if (-not $backupGUIDs.ContainsKey($guid)) {
                $orphanedHashes += $hashFile
            }
        }

        # Find orphaned metadata files
        $orphanedMetadata = @()
        foreach ($metadataFile in $metadataFiles) {
            $guid = $metadataFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
            if (-not $backupGUIDs.ContainsKey($guid)) {
                $orphanedMetadata += $metadataFile
            }
        }

        # Find orphaned pre-restore files
        $orphanedPreRestore = @()
        foreach ($preRestoreFile in $preRestoreFiles) {
            # Extract GUID from pre-restore filename (format: profile_GUID.backup.pre-restore)
            $guid = $preRestoreFile.BaseName -replace '^.*_([a-f0-9\-]+)\.backup$', '$1'
            if (-not $backupGUIDs.ContainsKey($guid)) {
                $orphanedPreRestore += $preRestoreFile
            }
        }

        $totalOrphaned = $orphanedHashes.Count + $orphanedMetadata.Count + $orphanedPreRestore.Count

        if ($totalOrphaned -eq 0) {
            Write-Host "✅ No orphaned files found." -ForegroundColor Green
            return
        }

        $orphanSummary = @()
        if ($orphanedHashes.Count -gt 0) { $orphanSummary += "$($orphanedHashes.Count) hash file(s)" }
        if ($orphanedMetadata.Count -gt 0) { $orphanSummary += "$($orphanedMetadata.Count) metadata file(s)" }
        if ($orphanedPreRestore.Count -gt 0) { $orphanSummary += "$($orphanedPreRestore.Count) pre-restore file(s)" }
        
        Write-Host "Found $($orphanSummary -join ', ')" -ForegroundColor Yellow

        # Remove orphaned files
        $removed = 0
        foreach ($hashFile in $orphanedHashes) {
            if ($PSCmdlet.ShouldProcess($hashFile.Name, 'Remove orphaned hash file')) {
                try {
                    Remove-Item -Path $hashFile.FullName -Force -ErrorAction Stop
                    $removed++
                    Write-Host "✅ Removed: $($hashFile.Name)" -ForegroundColor Green
                }
                catch {
                    Write-Error "❌ Could not remove: $($hashFile.Name)"
                }
            }
        }

        foreach ($metadataFile in $orphanedMetadata) {
            if ($PSCmdlet.ShouldProcess($metadataFile.Name, 'Remove orphaned metadata file')) {
                try {
                    Remove-Item -Path $metadataFile.FullName -Force -ErrorAction Stop
                    $removed++
                    Write-Host "✅ Removed: $($metadataFile.Name)" -ForegroundColor Green
                }
                catch {
                    Write-Error "❌ Could not remove: $($metadataFile.Name)"
                }
            }
        }

        foreach ($preRestoreFile in $orphanedPreRestore) {
            if ($PSCmdlet.ShouldProcess($preRestoreFile.Name, 'Remove orphaned pre-restore file')) {
                try {
                    Remove-Item -Path $preRestoreFile.FullName -Force -ErrorAction Stop
                    $removed++
                    Write-Host "✅ Removed: $($preRestoreFile.Name)" -ForegroundColor Green
                }
                catch {
                    Write-Error "❌ Could not remove: $($preRestoreFile.Name)"
                }
            }
        }

        Write-Host "`n✅ Cleanup complete. Removed $removed orphaned file(s)." -ForegroundColor Green
    }
    catch {
        Write-Error "❌ An error occurred while cleaning up orphaned files."
        throw $_.Exception.Message
    }
}