Public/Limit-RetainedItem.ps1

function Limit-RetainedItem {
<#
.SYNOPSIS
    Prunes filesystem items in a directory by age, count, or both.
 
.DESCRIPTION
    Out-of-the-box Windows has no logrotate equivalent and PowerShell
    has no built-in rolling-file primitive for retaining recent
    artifacts. This helper is the lightweight at-write-time sweep
    callers use to keep diagnostics / log / cache folders from
    growing without bound.
 
    Two retention dimensions, applied in order:
      1. -MaxAgeDays - drop anything older than the cutoff. 0 disables.
      2. -MaxItems - keep at most N most-recent items after the
                        age pass. 0 disables.
 
    Items are picked by LastWriteTime (not by name) so timestamped
    filenames are not load-bearing. Works on files and directories;
    -FileOnly lets callers scope to one or the other (useful for
    sweeping timestamped per-run diag folders).
 
    Safe to call against a missing directory (returns silently);
    safe to call against an empty directory (no-op). Glob filter
    applied via Get-ChildItem -Filter so only items matching the
    pattern get evaluated - never accidentally prunes neighbour
    artifacts.
 
.PARAMETER Directory
    Directory to sweep. Missing directories are tolerated as a
    no-op so callers do not need a Test-Path guard.
 
.PARAMETER Filter
    Wildcard filter applied to item names. Default '*' = every
    entry in the directory.
 
.PARAMETER MaxItems
    Keep at most this many most-recent items. 0 disables the
    count-based pass.
 
.PARAMETER MaxAgeDays
    Drop items older than N days. 0 disables the age-based pass.
 
.PARAMETER FileOnly
    When set, only files are considered. When clear, files and
    directories are both eligible (useful for sweeping timestamped
    per-run diag folders).
 
.EXAMPLE
    Limit-RetainedItem -Directory C:\logs -Filter '*.log' `
                       -MaxItems 20 -MaxAgeDays 30 -FileOnly
 
    Keep the 20 most-recent *.log files, drop anything older than
    30 days first.
 
.EXAMPLE
    Limit-RetainedItem -Directory C:\diag\<vm> `
                       -Filter '????-??-??_*' -MaxItems 30
 
    Sweep per-run timestamped subfolders (files AND directories),
    keep the 30 most recent.
#>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Directory,

        [string] $Filter = '*',

        [int] $MaxItems = 0,

        [int] $MaxAgeDays = 0,

        [switch] $FileOnly
    )

    if (-not (Test-Path -LiteralPath $Directory)) { return }

    $gciParams = @{
        Path        = $Directory
        Filter      = $Filter
        ErrorAction = 'SilentlyContinue'
    }
    if ($FileOnly) { $gciParams['File'] = $true }

    # Age-based pass: drop anything older than the cutoff.
    if ($MaxAgeDays -gt 0) {
        $cutoff = (Get-Date).AddDays(-$MaxAgeDays)
        Get-ChildItem @gciParams |
            Where-Object { $_.LastWriteTime -lt $cutoff } |
            ForEach-Object {
                if ($PSCmdlet.ShouldProcess($_.FullName, "Remove (older than $MaxAgeDays days)")) {
                    Remove-Item -LiteralPath $_.FullName -Recurse -Force `
                                -ErrorAction SilentlyContinue
                }
            }
    }

    # Count-based pass: keep the N most recent, drop the rest.
    if ($MaxItems -gt 0) {
        Get-ChildItem @gciParams |
            Sort-Object LastWriteTime -Descending |
            Select-Object -Skip $MaxItems |
            ForEach-Object {
                if ($PSCmdlet.ShouldProcess($_.FullName, "Remove (over -MaxItems=$MaxItems)")) {
                    Remove-Item -LiteralPath $_.FullName -Recurse -Force `
                                -ErrorAction SilentlyContinue
                }
            }
    }
}