functions/Remove-StaleFiles.ps1

function Remove-StaleFiles {
    <#
    .SYNOPSIS
        Remove stale files and directories recursively based on modification time.
    .DESCRIPTION
        Remove-StaleFiles recursively removes files and directories that have not been modified within a specified number of days.
        Files and directories older than the specified age are considered "stale" and will be removed.
        Logs are written to daily files in the user's temp directory with automatic cleanup.
    .PARAMETER Path
        The directory path to process. Defaults to the current user's temporary directory.
    .PARAMETER Age
        The number of days before a file or directory is considered stale. Files/directories not modified within this period will be removed.
    .PARAMETER Extension
        Optional file extension filter. Only files with this extension will be considered for removal.
    .PARAMETER Force
        Skip confirmation prompts and remove files without asking.
    .PARAMETER LogRetentionDays
        Number of days to keep daily log files. Defaults to 7 days.
    .EXAMPLE
        C:\PS> Remove-StaleFiles -Age 30 -Path "C:\Temp"
        Removes all files and directories in C:\Temp that haven't been modified in 30 days.
    .EXAMPLE
        C:\PS> Remove-StaleFiles -Age 7 -Extension ".log" -WhatIf
        Shows what .log files would be removed that are older than 7 days, without actually deleting them.
    .EXAMPLE
        C:\PS> Remove-StaleFiles -Age 14 -Force
        Removes all files in the temp directory older than 14 days without confirmation.
    .EXAMPLE
        C:\PS> Remove-StaleFiles -Age 30 -LogRetentionDays 14
        Removes stale files and keeps log files for 14 days instead of the default 7.
    .OUTPUTS
        Summary of removed files and directories.
    .NOTES
        Version 1.1.0
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false, HelpMessage = 'Directory path to process')]
        [string] $Path = [System.IO.Path]::GetTempPath(),
        [Parameter(Mandatory = $true, HelpMessage = 'Number of days before a file is considered stale')]
        [int] $Age,
        [Parameter(Mandatory = $false, HelpMessage = 'File extension filter')]
        [string] $Extension,
        [Parameter(Mandatory = $false)]
        [switch] $Force,
        [Parameter(Mandatory = $false, HelpMessage = 'Number of days to keep log files')]
        [int] $LogRetentionDays = 7
    )
    $ErrorActionPreference = 'Stop'
    $InformationPreference = 'Continue'
    $StartTime = Get-Date

    # Set up logging - use appropriate temp directory for the platform.
    $TempDir = [System.IO.Path]::GetTempPath()
    $LogDir = Join-Path $TempDir 'PSF-Module/Logs'
    $LogName = 'RemoveStaleFiles'
    $DateSuffix = Get-Date -Format 'yyyy-MM-dd'
    $LogPath = Join-Path $LogDir ('{0}-{1}.log' -f $LogName, $DateSuffix)

    function Write-StaleFileLog {
        param(
            [string] $Message,
            [string] $Level = 'INFO'
        )

        if (-not (Test-Path $LogDir)) {
            $null = New-Item -Path $LogDir -ItemType Directory -Force
        }

        $Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
        $LogEntry = '{0} [{1}] {2}' -f $Timestamp, $Level, $Message
        $LogEntry | Out-File -FilePath $LogPath -Append -Encoding UTF8
    }

    function Clear-OldLogs {
        param(
            [string] $LogDirectory,
            [string] $LogPrefix,
            [int] $RetentionDays
        )

        if (Test-Path $LogDirectory) {
            $CutoffDate = (Get-Date).AddDays(-$RetentionDays)
            Get-ChildItem -Path $LogDirectory -Filter ('{0}-*.log' -f $LogPrefix) |
                Where-Object { $_.LastWriteTime -lt $CutoffDate } |
                ForEach-Object {
                    try {
                        Remove-Item -Path $_.FullName -Force
                        Write-Verbose ('Cleaned up old log file: {0}' -f $_.Name)
                    }
                    catch {
                        Write-Warning ('Failed to remove old log file {0}: {1}' -f $_.Name, $_.Exception.Message)
                    }
                }
        }
    }

    # Clean up old logs.
    Clear-OldLogs -LogDirectory $LogDir -LogPrefix $LogName -RetentionDays $LogRetentionDays

    # Log function start.
    $LogParams = @(
        ('Path: {0}' -f $Path)
        ('Age: {0} days' -f $Age)
    )
    if ($Extension) { $LogParams += ('Extension: {0}' -f $Extension) }
    if ($Force) { $LogParams += 'Force: True' }
    if ($WhatIfPreference) { $LogParams += 'WhatIf: True' }

    Write-StaleFileLog ('STARTED Remove-StaleFiles - {0}' -f ($LogParams -join ', '))

    if (-not (Test-Path -Path $Path)) {
        $ErrorMsg = ('Path ''{0}'' does not exist.' -f $Path)
        Write-StaleFileLog $ErrorMsg 'ERROR'
        Write-Error $ErrorMsg
        return
    }

    $CutoffDate = (Get-Date).AddDays(-$Age)

    Write-Verbose ('Scanning for files older than {0} days (before {1})' -f $Age, $CutoffDate.ToString('yyyy-MM-dd HH:mm:ss'))
    Write-Verbose ('Processing path: {0}' -f $Path)

    # Get all items recursively and filter by age and extension.
    try {
        $AllItems = Get-ChildItem -Path $Path -Recurse -Force
    }
    catch {
        $ErrorMsg = ('Unable to access directory: {0} - {1}' -f $Path, $_.Exception.Message)
        Write-StaleFileLog $ErrorMsg 'ERROR'
        Write-Warning $ErrorMsg
        return
    }

    $ItemsToRemove = @()

    # Process files first.
    $StaleFiles = $AllItems | Where-Object {
        -not $_.PSIsContainer -and
        $_.LastWriteTime -lt $CutoffDate -and
        (-not $Extension -or $_.Extension -eq $Extension)
    }
    $ItemsToRemove += $StaleFiles

    # Process directories (empty ones that are stale).
    $StaleDirectories = $AllItems | Where-Object {
        $_.PSIsContainer -and
        $_.LastWriteTime -lt $CutoffDate
    } | Where-Object {
        try {
            $ChildItems = Get-ChildItem -Path $_.FullName -Force
            $ChildItems.Count -eq 0
        }
        catch {
            Write-Warning ('Unable to check contents of directory: {0} - {1}' -f $_.FullName, $_.Exception.Message)
            $false
        }
    }
    $ItemsToRemove += $StaleDirectories

    if ($ItemsToRemove.Count -eq 0) {
        Write-StaleFileLog 'No stale files found'
        Write-Information 'No stale files found.'
        return
    }

    $Files = $ItemsToRemove | Where-Object { -not $_.PSIsContainer }
    $Directories = $ItemsToRemove | Where-Object { $_.PSIsContainer }

    Write-Information ('Found {0} stale items:' -f $ItemsToRemove.Count)
    Write-StaleFileLog ('Found {0} stale items: {1} files, {2} directories' -f $ItemsToRemove.Count, $Files.Count, $Directories.Count)

    if ($Files.Count -gt 0) {
        Write-Information (' Files: {0}' -f $Files.Count)
        if ($VerbosePreference -eq 'Continue') {
            $Files | ForEach-Object { Write-Verbose (' {0} ({1})' -f $_.FullName, (Get-Date $_.LastWriteTime -Format 'yyyy-MM-dd HH:mm:ss')) }
        }
    }

    if ($Directories.Count -gt 0) {
        Write-Information (' Directories: {0}' -f $Directories.Count)
        if ($VerbosePreference -eq 'Continue') {
            $Directories | ForEach-Object { Write-Verbose (' {0} ({1})' -f $_.FullName, (Get-Date $_.LastWriteTime -Format 'yyyy-MM-dd HH:mm:ss')) }
        }
    }

    if ($WhatIfPreference) {
        Write-StaleFileLog ('WhatIf mode: Would remove {0} stale items' -f $ItemsToRemove.Count)
        Write-Information ('WhatIf: Would remove {0} stale items.' -f $ItemsToRemove.Count)
        return
    }

    if (-not $Force) {
        $Response = Read-Host ('Do you want to remove these {0} items? (y/N)' -f $ItemsToRemove.Count)
        if ($Response -ne 'y' -and $Response -ne 'Y') {
            Write-StaleFileLog 'Operation cancelled by user'
            Write-Information 'Operation cancelled.'
            return
        }
    }

    $RemovedCount = 0
    $FailedCount = 0

    foreach ($Item in $ItemsToRemove) {
        if ($PSCmdlet.ShouldProcess($Item.FullName, 'Remove')) {
            try {
                $ItemType = if ($Item.PSIsContainer) { 'Directory' } else { 'File' }
                $ItemSize = if (-not $Item.PSIsContainer) { $Item.Length } else { 0 }

                if ($Item.PSIsContainer) {
                    Remove-Item -Path $Item.FullName -Force -Recurse
                }
                else {
                    Remove-Item -Path $Item.FullName -Force
                }
                $RemovedCount++

                Write-Verbose ('Removed: {0}' -f $Item.FullName)
                Write-StaleFileLog ('REMOVED {0}: {1} (Last Modified: {2}, Size: {3} bytes)' -f $ItemType, $Item.FullName, $Item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'), $ItemSize)
            }
            catch {
                $FailedCount++
                $ErrorMsg = ('Failed to remove ''{0}'': {1}' -f $Item.FullName, $_.Exception.Message)
                Write-Warning $ErrorMsg
                Write-StaleFileLog $ErrorMsg 'ERROR'
            }
        }
    }

    # Log completion summary.
    $Duration = (Get-Date) - $StartTime
    $SummaryMsg = ('COMPLETED: Successfully removed {0} items, {1} failures in {2:F2} seconds' -f $RemovedCount, $FailedCount, $Duration.TotalSeconds)
    Write-StaleFileLog $SummaryMsg

    Write-Information 'Removal complete:'
    Write-Information (' Successfully removed: {0} items' -f $RemovedCount)
    if ($FailedCount -gt 0) {
        Write-Information (' Failed to remove: {0} items' -f $FailedCount)
    }
}