Functions/Public/Remove-StaleFile.ps1

function Remove-StaleFile {
    <#
    .SYNOPSIS
        Removes old files from a folder while keeping the newest ones.
 
    .DESCRIPTION
        The Remove-StaleFile function helps manage disk space by automatically cleaning up old files
        while preserving the most recent ones. It supports multiple filter patterns and can group
        files by name patterns to keep the newest files for each group separately.
 
        Files that are currently locked by another process are automatically skipped with a warning.
 
    .PARAMETER Path
        Specifies the folder path to process. The path must exist or an error is returned.
 
    .PARAMETER Keep
        Specifies the number of newest files to keep. Files beyond this count (sorted by
        LastWriteTime descending) are deleted. Default is 5.
 
    .PARAMETER Filter
        Specifies file filter pattern(s) to match. Accepts wildcards.
        Examples: '*.xlsx', '*.log', or an array like @('*.xlsx', '*.csv')
 
    .PARAMETER NamePattern
        Specifies optional filename pattern(s) to group files by before applying retention.
        When specified, the function keeps the newest files for EACH pattern separately.
        Examples: 'Report-*', 'Backup_*', or @('Report-*', 'Audit-*')
 
    .PARAMETER WhatIf
        Shows what would happen if the cmdlet runs. The cmdlet is not run.
 
    .PARAMETER Confirm
        Prompts you for confirmation before running the cmdlet.
 
    .INPUTS
        None. You cannot pipe objects to Remove-StaleFile.
 
    .OUTPUTS
        None. This function does not generate any output.
 
    .EXAMPLE
        Remove-StaleFile -Path "C:\Logs" -Filter "*.log" -Keep 10
 
        Keeps the 10 newest .log files in C:\Logs and deletes the rest.
 
    .EXAMPLE
        Remove-StaleFile -Path "C:\Output" -Filter @('*.xlsx', '*.csv') -Keep 5
 
        Keeps the 5 newest Excel and CSV files combined (sorted by LastWriteTime).
 
    .EXAMPLE
        Remove-StaleFile -Path "C:\Reports" -Filter "*.csv" -NamePattern "DailyReport-*" -Keep 7
 
        Keeps only the 7 newest files matching 'DailyReport-*.csv' pattern.
 
    .EXAMPLE
        Remove-StaleFile -Path "C:\Backups" -Filter "*.zip" -NamePattern @('Full-*', 'Incremental-*') -Keep 3
 
        Keeps the 3 newest 'Full-*.zip' files AND the 3 newest 'Incremental-*.zip' files separately.
 
    .EXAMPLE
        Remove-StaleFile -Path "C:\Logs" -Filter "*.log" -Keep 5 -WhatIf
 
        Shows which files would be deleted without actually deleting them.
 
    .NOTES
        Author: Sune Alexandersen Narud
        Version: 1.0.0
        Date: February 5, 2026
 
        Requires:
        - PowerShell 5.1 or later
         
        Optional:
        - OrionDesign module for enhanced output formatting (Write-Action, Get-OrionTheme)
 
    .LINK
        Test-FileLock
 
    .LINK
        https://github.com/suneworld/OrionUtils
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true, 
                   Position = 0,
                   HelpMessage = "The folder path containing files to clean up.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$Path,
        
        [Parameter(Position = 1,
                   HelpMessage = "Number of newest files to keep. Default is 5.")]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$Keep = 5,

        [Parameter(Mandatory = $true,
                   Position = 2,
                   HelpMessage = "File filter pattern(s) like '*.log' or @('*.xlsx', '*.csv')")]
        [ValidateNotNullOrEmpty()]
        [string[]]$Filter,

        [Parameter(HelpMessage = "Optional filename pattern(s) to group files by for separate retention.")]
        [string[]]$NamePattern = @()
    )

    begin {
        Write-Verbose "Starting Remove-StaleFile on path: $Path"
        Write-Verbose "Keeping $Keep newest file(s) matching: $($Filter -join ', ')"
        
        # Get folder name for display
        $folderName = Split-Path -Leaf $Path
    }

    process {
        # Get all files matching the filters
        $allFiles = @()
        foreach ($filterPattern in $Filter) {
            $matchingFiles = Get-ChildItem -Path $Path -Filter $filterPattern -File -ErrorAction SilentlyContinue
            $allFiles += $matchingFiles
        }

        # Remove duplicates (in case patterns overlap)
        $allFiles = $allFiles | Sort-Object FullName -Unique

        if ($allFiles.Count -eq 0) {
            if (Get-Command -Name 'Write-Action' -ErrorAction SilentlyContinue) {
                Write-Action "No files found matching the specified filters: $($Filter -join ', ')" -Complete
            }
            else {
                Write-Host " No files found matching: $($Filter -join ', ')" -ForegroundColor Yellow
            }
            return
        }

        Write-Verbose "Found $($allFiles.Count) file(s) matching filters"

        # Process files - either by name patterns or as a single group
        if ($NamePattern.Count -gt 0) {
            # Group files by name patterns and process each group separately
            foreach ($pattern in $NamePattern) {
                $groupFiles = $allFiles | Where-Object { $_.Name -like $pattern } | Sort-Object LastWriteTime -Descending
                
                if ($groupFiles.Count -eq 0) {
                    if (Get-Command -Name 'Write-Action' -ErrorAction SilentlyContinue) {
                        Write-Action "No files found matching pattern '$pattern'" -Complete
                    }
                    else {
                        Write-Host " No files found matching pattern: $pattern" -ForegroundColor Yellow
                    }
                    continue
                }

                Write-Host "Processing files matching pattern: $pattern" -ForegroundColor Cyan
                Write-Verbose "Found $($groupFiles.Count) file(s) for pattern: $pattern"
                
                if ($groupFiles.Count -le $Keep) {
                    $mutedColor = 'DarkGray'
                    if (Get-Command -Name 'Get-OrionTheme' -ErrorAction SilentlyContinue) {
                        try {
                            $theme = Get-OrionTheme
                            if ($theme.Muted) { $mutedColor = $theme.Muted }
                        }
                        catch { }
                    }
                    Write-Host " $folderName\$pattern - Nothing to delete ($($groupFiles.Count) file(s), keeping $Keep)" -ForegroundColor $mutedColor
                    continue
                }

                # Select files to delete for this group
                $filesToDelete = $groupFiles | Select-Object -Skip $Keep
                
                foreach ($file in $filesToDelete) {
                    if ($PSCmdlet.ShouldProcess($file.FullName, "Delete file")) {
                        Remove-FileWithLockCheck -FilesToDelete $file
                    }
                }
            }
        }
        else {
            # Process all files as a single group (original behavior)
            $files = $allFiles | Sort-Object LastWriteTime -Descending

            if ($files.Count -le $Keep) {
                $mutedColor = 'DarkGray'
                if (Get-Command -Name 'Get-OrionTheme' -ErrorAction SilentlyContinue) {
                    try {
                        $theme = Get-OrionTheme
                        if ($theme.Muted) { $mutedColor = $theme.Muted }
                    }
                    catch { }
                }
                Write-Host " $folderName\$($Filter -join ', ') - Nothing to delete ($($files.Count) file(s), keeping $Keep)" -ForegroundColor $mutedColor
                return
            }

            # Select files to delete
            $filesToDelete = $files | Select-Object -Skip $Keep
            Write-Verbose "Deleting $($filesToDelete.Count) file(s)"
            
            foreach ($file in $filesToDelete) {
                if ($PSCmdlet.ShouldProcess($file.FullName, "Delete file")) {
                    Remove-FileWithLockCheck -FilesToDelete $file
                }
            }
        }
    }

    end {
        Write-Verbose "Remove-StaleFile completed"
    }
}