public/Remove-PSProfileSnapshot.ps1

<#
    .SYNOPSIS
        Removes snapshots and all associated files.
    .DESCRIPTION
        Deletes profile snapshots based on various criteria. You can remove snapshots
        by name, GUID, or apply retention policies to automatically clean up old snapshots.
        Retention policies allow you to keep only the most recent N snapshots or
        snapshots created within a certain timespan.
        
        When removing a snapshot, ALL associated files are deleted:
        - Backup file (.backup)
        - Hash file (.hash)
        - Metadata file (.metadata)
        - Pre-restore safety backup (.backup.pre-restore)
        
        This prevents orphaned files from accumulating on the filesystem.
    .PARAMETER Name
        The name of a specific snapshot to remove. Can be piped from Get-PSProfileSnapshot.
    .PARAMETER GUID
        The GUID of a specific snapshot to remove. Can be piped from Get-PSProfileSnapshot.
    .PARAMETER KeepLast
        Remove all snapshots except the most recent N snapshots.
        For example, -KeepLast 3 will keep only the 3 most recent snapshots.
    .PARAMETER OlderThan
        Remove snapshots older than the specified timespan.
        For example, -OlderThan (New-TimeSpan -Days 30) removes snapshots older than 30 days.
    .INPUTS
        None
    .OUTPUTS
        None
    .EXAMPLE
        Remove-PSProfileSnapshot -Name "old-config"

        Removes the snapshot named "old-config" and all associated files.
    .EXAMPLE
        Get-PSProfileSnapshot | Where-Object Name -EQ "old-config" | Remove-PSProfileSnapshot

        Removes snapshot by piping from Get-PSProfileSnapshot.
    .EXAMPLE
        Remove-PSProfileSnapshot -KeepLast 5

        Removes all snapshots except the 5 most recent ones.
    .EXAMPLE
        Remove-PSProfileSnapshot -OlderThan (New-TimeSpan -Days 30)

        Removes all snapshots older than 30 days.
    .NOTES
        Be cautious when using retention policies. They will remove multiple snapshots.
        Use -WhatIf to preview what would be deleted before executing.
#>


function Remove-PSProfileSnapshot {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName = $true)]
        [Alias('SnapshotName')]
        [string]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByGUID', ValueFromPipelineByPropertyName = $true)]
        [Alias('SnapshotGUID')]
        [guid]$GUID,

        [Parameter(Mandatory = $true, ParameterSetName = 'KeepLast')]
        [int]$KeepLast,

        [Parameter(Mandatory = $true, ParameterSetName = 'OlderThan')]
        [timespan]$OlderThan
    )

    try {
        $profilesPath = Split-Path -Path $profile -Parent
        $backupFiles = Get-ChildItem -Path $profilesPath -Filter "*.backup" -ErrorAction Stop | 
            Sort-Object -Property CreationTime -Descending

        if (-not $backupFiles) {
            Write-Host "ℹ️ No snapshots found to remove." -ForegroundColor Cyan
            return
        }

        $snapshotsToRemove = @()

        switch ($PSCmdlet.ParameterSetName) {
            'ByName' {
                # Remove by snapshot name (either custom name from metadata or timestamp)
                foreach ($backupFile in $backupFiles) {
                    $guid = $backupFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                    $metadataFileName = "$($profile)_$($guid).metadata"
                    
                    # Check metadata first for custom name
                    if (Test-Path -Path $metadataFileName -ErrorAction SilentlyContinue) {
                        $metadata = Get-Content -Path $metadataFileName -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue
                        if ($metadata.Name -eq $Name) {
                            $snapshotsToRemove += @{
                                BackupFile = $backupFile
                                GUID       = $guid
                            }
                            break
                        }
                    }
                    
                    # Also check if name matches the timestamp format of CreatedTime
                    $timestampName = $backupFile.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
                    if ($timestampName -eq $Name) {
                        $snapshotsToRemove += @{
                            BackupFile = $backupFile
                            GUID       = $guid
                        }
                        break
                    }
                }

                if ($snapshotsToRemove.Count -eq 0) {
                    Write-Host "⚠️ No snapshot found with name: $SnapshotName" -ForegroundColor Yellow
                    return
                }
            }

            'ByGUID' {
                # Remove by GUID
                foreach ($backupFile in $backupFiles) {
                    $guid = $backupFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                    if ($guid -eq $SnapshotGUID.ToString()) {
                        $snapshotsToRemove += @{
                            BackupFile = $backupFile
                            GUID       = $guid
                        }
                        break
                    }
                }

                if ($snapshotsToRemove.Count -eq 0) {
                    Write-Host "⚠️ No snapshot found with GUID: $SnapshotGUID" -ForegroundColor Yellow
                    return
                }
            }

            'KeepLast' {
                # Keep only the last N snapshots
                if ($KeepLast -lt 1) {
                    Write-Error "❌ KeepLast must be at least 1."
                    return
                }

                if ($backupFiles.Count -le $KeepLast) {
                    Write-Host "ℹ️ You have $($backupFiles.Count) snapshot(s); no removal needed." -ForegroundColor Cyan
                    return
                }

                # Remove all but the last N
                $snapshotsToRemove = @()
                for ($i = $KeepLast; $i -lt $backupFiles.Count; $i++) {
                    $guid = $backupFiles[$i].BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                    $snapshotsToRemove += @{
                        BackupFile = $backupFiles[$i]
                        GUID       = $guid
                    }
                }
            }

            'OlderThan' {
                # Remove snapshots older than the specified timespan
                $cutoffTime = (Get-Date) - $OlderThan

                foreach ($backupFile in $backupFiles) {
                    if ($backupFile.CreationTime -lt $cutoffTime) {
                        $guid = $backupFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                        $snapshotsToRemove += @{
                            BackupFile = $backupFile
                            GUID       = $guid
                        }
                    }
                }

                if ($snapshotsToRemove.Count -eq 0) {
                    Write-Host "ℹ️ No snapshots found older than $($OlderThan.ToString())." -ForegroundColor Cyan
                    return
                }
            }
        }

        # Remove the identified snapshots and ALL associated files
        foreach ($snapshot in $snapshotsToRemove) {
            $backupFile = $snapshot.BackupFile
            $guid = $snapshot.GUID
            $hashFileName = "$($profile)_$($guid).hash"
            $metadataFileName = "$($profile)_$($guid).metadata"
            $preRestoreFileName = "$($profile)_$($guid).backup.pre-restore"

            $actionTarget = "$($backupFile.Name) and associated files (hash, metadata, pre-restore)"
            if ($PSCmdlet.ShouldProcess($actionTarget, 'Remove')) {
                $filesRemoved = 0
                try {
                    # Remove backup file
                    if (Test-Path -Path $backupFile.FullName -ErrorAction SilentlyContinue) {
                        Remove-Item -Path $backupFile.FullName -Force -ErrorAction Stop
                        $filesRemoved++
                    }

                    # Remove hash file if it exists
                    if (Test-Path -Path $hashFileName -ErrorAction SilentlyContinue) {
                        Remove-Item -Path $hashFileName -Force -ErrorAction Stop
                        $filesRemoved++
                    }

                    # Remove metadata file if it exists
                    if (Test-Path -Path $metadataFileName -ErrorAction SilentlyContinue) {
                        Remove-Item -Path $metadataFileName -Force -ErrorAction Stop
                        $filesRemoved++
                    }

                    # Remove pre-restore safety backup if it exists
                    if (Test-Path -Path $preRestoreFileName -ErrorAction SilentlyContinue) {
                        Remove-Item -Path $preRestoreFileName -Force -ErrorAction Stop
                        $filesRemoved++
                    }

                    Write-Host "✅ Removed snapshot and $filesRemoved associated file(s): $($backupFile.Name)" -ForegroundColor Green
                }
                catch {
                    Write-Error "❌ Could not remove snapshot: $($backupFile.Name)"
                }
            }
        }

        Write-Host "`n✅ Snapshot removal complete." -ForegroundColor Green
    }
    catch {
        Write-Error "❌ An error occurred while removing snapshots."
        throw $_.Exception.Message
    }
}