Public/Remove-DailyBackup.ps1

function Remove-DailyBackup
{
    <#
    .SYNOPSIS
        Removes daily backup directories and files based on specified criteria.

    .DESCRIPTION
        Removes daily backup directories and files from a backup location. Supports multiple
        removal modes including date-based cleanup, retention policies, and specific backup
        removal. The function can clean up old backups while preserving recent ones, or
        remove specific backup dates. Only directories matching the yyyy-MM-dd date pattern
        are considered for removal operations.

    .PARAMETER Path
        The root directory path where daily backup folders are stored, or the specific
        backup directory to remove when using -Date parameter. This should be the parent
        directory containing date-named subdirectories (e.g., '2025-08-24').

    .PARAMETER Keep
        The minimum number of backup directories to retain when using retention-based cleanup.
        Older backups beyond this number will be deleted, sorted by date with oldest removed
        first. Set to 0 to remove all backups. Cannot be used with -Date parameter.

        Note: BackupsToKeep is an alias for this parameter.

    .PARAMETER Date
        Specific backup date to remove (yyyy-MM-dd format). When specified, only the backup
        directory for this date will be removed. Cannot be used with -Keep parameter.

    .PARAMETER Force
        Bypass confirmation prompts and remove backups without user interaction. Use with
        caution as this will permanently delete backup data.

    .INPUTS
        [String]
        Backup root path can be piped to this function.

    .OUTPUTS
        None. This function does not return any objects but may display verbose information
        about the removal process.

    .NOTES
        - Only directories matching the yyyy-MM-dd date pattern are processed
        - Supports ShouldProcess for WhatIf and Confirm functionality
        - Uses cross-platform compatible removal methods
        - Continues operation even if individual directory deletions fail
        - Verbose output provides detailed information about removal operations

    .EXAMPLE
        PS > Remove-DailyBackup -Path 'C:\Backups' -Keep 7

        Keeps the 7 most recent daily backup folders, removes older ones

    .EXAMPLE
        PS > Remove-DailyBackup -Path 'C:\Backups' -Date '2025-09-01'

        Removes only the backup directory for September 1, 2025

    .EXAMPLE
        PS > Remove-DailyBackup -Path '/home/user/backups' -Keep 3 -WhatIf

        Shows which backup directories would be deleted without actually removing them

    .EXAMPLE
        PS > Remove-DailyBackup -Path 'D:\Backups' -Keep 0 -Force

        Removes all backup directories without confirmation prompts

    .EXAMPLE
        PS > 'C:\MyBackups' | Remove-DailyBackup -Keep 14

        Pipeline input: maintains a 2-week retention policy (14 days) for backup directories
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Retention')]
    param
    (
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'The root directory path where daily backup folders are stored.'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('BackupRoot', 'DestinationPath')]
        [string] $Path,

        [Parameter(
            ParameterSetName = 'Retention',
            HelpMessage = 'The number of daily backup directories to retain.'
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [Alias('BackupsToKeep')]
        [int] $Keep = 7,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'SpecificDate',
            HelpMessage = 'Specific backup date to remove (yyyy-MM-dd format).'
        )]
        [ValidatePattern('^\d{4}-\d{2}-\d{2}$')]
        [string] $Date,

        [Parameter(
            HelpMessage = 'Bypass confirmation prompts and remove backups without user interaction.'
        )]
        [switch] $Force
    )

    begin
    {
        Write-Verbose 'Remove-DailyBackup> Starting backup removal operation'
    }

    process
    {
        # Validate input path first
        if ([string]::IsNullOrWhiteSpace($Path))
        {
            Write-Error 'Remove-DailyBackup> Path parameter cannot be null or empty'
            return
        }

        # Normalize and resolve the input path
        $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

        Write-Verbose "Remove-DailyBackup> Input path: '$Path'"

        # Resolve the path to ensure it exists
        try
        {
            $validatedBackupRootPath = Resolve-Path -LiteralPath $Path -ErrorAction Stop
            $normalizedBackupRootPath = $validatedBackupRootPath.Path
        }
        catch
        {
            Write-Error "Remove-DailyBackup> Cannot access path '$Path': $($_.Exception.Message)"
            return
        }

        Write-Verbose "Remove-DailyBackup> Processing backup root: $normalizedBackupRootPath"

        # Get qualified backup directories (matching yyyy-MM-dd pattern)
        $dateMatchingBackupDirectories = @(Get-ChildItem -LiteralPath $normalizedBackupRootPath -Directory -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -match '^\d{4}-\d{2}-\d{2}$' })

        if ($dateMatchingBackupDirectories.Length -eq 0)
        {
            Write-Verbose "Remove-DailyBackup> No qualified backup directories found in: $normalizedBackupRootPath"
            return
        }

        Write-Verbose "Remove-DailyBackup> Found $($dateMatchingBackupDirectories.Length) qualified backup directories"

        if ($PSCmdlet.ParameterSetName -eq 'SpecificDate')
        {
            # Remove specific date
            $specificDateBackupDirectory = $dateMatchingBackupDirectories | Where-Object { $_.Name -eq $Date }
            if (-not $specificDateBackupDirectory)
            {
                Write-Warning "Remove-DailyBackup> No backup found for date: $Date"
                return
            }

            $removalConfirmationPrompt = "Remove backup directory for date $Date"
            if ($Force -or $PSCmdlet.ShouldProcess($specificDateBackupDirectory.FullName, $removalConfirmationPrompt))
            {
                Write-Verbose "Remove-DailyBackup> Removing backup directory: $($specificDateBackupDirectory.FullName)"
                try
                {
                    Remove-ItemAlternative -LiteralPath $specificDateBackupDirectory.FullName -WhatIf:$WhatIfPreference
                    Write-Verbose "Remove-DailyBackup> Successfully removed: $($specificDateBackupDirectory.FullName)"
                }
                catch
                {
                    Write-Error "Remove-DailyBackup> Failed to remove directory '$($specificDateBackupDirectory.FullName)': $($_.Exception.Message)"
                }
            }
        }
        else
        {
            # Retention-based cleanup
            if ($dateMatchingBackupDirectories.Length -le $Keep)
            {
                Write-Verbose "Remove-DailyBackup> Current backup count ($($dateMatchingBackupDirectories.Length)) does not exceed retention limit ($Keep)"
                return
            }

            # Create hashtable to sort backup directories by date
            $backupDirectoriesWithDates = @{ }
            foreach ($currentBackupDirectory in $dateMatchingBackupDirectories)
            {
                try
                {
                    $backupDirectoriesWithDates.Add($currentBackupDirectory.FullName, [System.DateTime]::ParseExact($currentBackupDirectory.Name, 'yyyy-MM-dd', $null))
                }
                catch
                {
                    Write-Warning "Remove-DailyBackup> Skipping directory with invalid date format: $($currentBackupDirectory.Name)"
                }
            }

            # Sort by date and remove oldest backups
            $backupDirectoriesOrderedByDate = ($backupDirectoriesWithDates.GetEnumerator() | Sort-Object -Property Value | ForEach-Object { $_.Key })
            $numberOfDirectoriesToRemove = $backupDirectoriesOrderedByDate.Count - $Keep

            Write-Verbose "Remove-DailyBackup> Will remove $numberOfDirectoriesToRemove old backup directories (keeping $Keep)"

            for ($removalIndex = 0; $removalIndex -lt $numberOfDirectoriesToRemove; $removalIndex++)
            {
                $currentDirectoryPath = $backupDirectoriesOrderedByDate[$removalIndex]
                $currentDirectoryDate = Split-Path -Leaf $currentDirectoryPath

                $removalConfirmationPrompt = "Remove old backup directory for date $currentDirectoryDate"
                if ($Force -or $PSCmdlet.ShouldProcess($currentDirectoryPath, $removalConfirmationPrompt))
                {
                    Write-Verbose "Remove-DailyBackup> Removing old backup directory: $currentDirectoryPath"
                    try
                    {
                        Remove-ItemAlternative -LiteralPath $currentDirectoryPath -WhatIf:$WhatIfPreference
                        Write-Verbose "Remove-DailyBackup> Successfully removed: $currentDirectoryPath"
                    }
                    catch
                    {
                        Write-Error "Remove-DailyBackup> Failed to remove directory '$currentDirectoryPath': $($_.Exception.Message)"
                    }
                }
            }
        }
    }

    end
    {
        Write-Verbose 'Remove-DailyBackup> Backup removal operation completed'
    }
}