RemoveOldFiles.psm1

function Remove-OldFiles {
    <#
    .SYNOPSIS
         
            Removes files that were last modified before the specified number of milliseconds,
            seconds, minutes, hours, days, weeks or months back in time - or a combination of any
            of these. The "LastWriteTime" property as returned from Get-ChildItem or Get-Item is
            used.
         
            Optional file name filtering with a regular expression.
 
            Copyright 2018-present, Joakim Borger Svendsen. All right reserved.
            Svendsen Tech.
 
            MIT license.
 
        .DESCRIPTION
         
            The times specified from ms to months are all cumulated and added together to
            form a large decimal-typed millisecond number. Months are handled with .NET date
            math as calendar months converted to milliseconds.
 
            You can specify a -NameFilterRegex parameter to only target only file names
            that match this regular expression. The default is simply ".*", which always matches,
            so name filtering is in effect not enabled by default unless you override with this
            parameter. NB! It is not wildcards such as for the -like operator, but regular
            expressions, so make sure you test and know that the results are as expected.
 
            It supports -WhatIf in a non-proper implementation currently (but functionally not
            too different, it was just a bit easier and also caused by a now gone, dead end in code).
 
        .PARAMETER Path
            Path or paths to process.
 
        .PARAMETER LiteralPath
            Path or paths to process using a literal path where brackets in names do not cause
            problems in PowerShell version 2.
 
        .PARAMETER Recurse
            Traverse directory structure recursively, otherwise only the first level is processed
            if the specified path is a directory. Only files are targeted if using, say, -Path x:\finance\*.log
 
        .PARAMETER Millisecond
            Delete files that were last modified longer ago than the specified number of milliseconds.
            This number is cumulatively added up with all other potentially specified times.
 
        .PARAMETER Second
            Delete files that were last modified longer ago than the specified number of seconds.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER Minute
            Delete files that were last modified longer ago than the specified number of minutes.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER Hour
            Delete files that were last modified longer ago than the specified number of hours.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER Day
            Delete files that were last modified longer ago than the specified number of days.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER Week
            Delete files that were last modified longer ago than the specified number of weeks.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER Month
            Delete files that were last modified longer ago than the specified number of months.
            This number is cumulatively added up with all other potentially specified times.
         
        .PARAMETER WhatIf
            A non-standard implementation of the -WhatIf parameter (I might fix it some time).
 
        .PARAMETER NameRegexMatch
            A regular expression the (Get-Item <current_element>).Name property must match.
            NB! Not wildcards such as for -like, but regular expression, such as for -match.
     
        .EXAMPLE
            Remove-OldFiles -Path . -WhatIf -Second 20 -Recurse -NameRegexMatch '\.tmp$'
 
            Removes all files with the extension ".tmp" that were modified more than 20
            seconds ago if you leave out -WhatIf.
 
        .EXAMPLE
            Remove-OldFiles -Path . -Minute 1 -Second 20 -Recurse -NameRegexMatch '^startswiththis'
 
            Removes all files in the current directory that were modified more than 1 minute
            and twenty seconds ago. Not full minutes or seconds above; even one minute twenty
            seconds and one single millisecond extra since last modification will trigger removal.
 
        .EXAMPLE
        PS C:\temp\testdir> Remove-OldFiles -Path . -Minute 17 -Recurse -NameRegexMatch '\.tmp$'
 
        VERBOSE: Recorded 'now' time as: 2018-12-16 21:11:27
        VERBOSE: Total milliseconds: 1020000.
        0 months.
        0 weeks.
        0 days.
        0 hours.
        17 minutes.
        0 seconds.
        0 milliseconds.
        VERBOSE: Processing path '.'.
        VERBOSE: in foreach processing 'C:\temp\testdir'.
        VERBOSE: Processing first level directory with path: 'C:\temp\testdir'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\deletethese1.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\3\deletethese1.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\deletethese2.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\3\deletethese2.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\deletethese3.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\3\deletethese3.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\deletethese1.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\deletethese1.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\deletethese2.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\deletethese2.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\deletethese3.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\2\deletethese3.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\deletethese1.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\deletethese1.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\deletethese2.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\deletethese2.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\deletethese3.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\1\deletethese3.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\deletethese1.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\deletethese1.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\deletethese2.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\deletethese2.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\deletethese3.tmp'.
        VERBOSE: Removing file 'C:\temp\testdir\deletethese3.tmp' because it was modified more than 1020000 ms ago and matches the specified name regex: '\.tmp$' (default '.*').
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese3.log'.
        VERBOSE: Total processed path count was: 28
        VERBOSE: Attempted to delete 12 files.
 
 
        PS C:\temp\testdir> Remove-OldFiles -Path . -Minute 17 -Recurse -NameRegexMatch '\.tmp$'
 
        VERBOSE: Recorded 'now' time as: 2018-12-16 21:11:38
        VERBOSE: Total milliseconds: 1020000.
        0 months.
        0 weeks.
        0 days.
        0 hours.
        17 minutes.
        0 seconds.
        0 milliseconds.
        VERBOSE: Processing path '.'.
        VERBOSE: in foreach processing 'C:\temp\testdir'.
        VERBOSE: Processing first level directory with path: 'C:\temp\testdir'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\3\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\2\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\1\keepthese3.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese1.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese2.log'.
        VERBOSE: in foreach processing 'C:\temp\testdir\keepthese3.log'.
        VERBOSE: Total processed path count was: 16
        VERBOSE: Attempted to delete 0 files.
 
 
        .LINK
            https://github.com/EliteLoser/RemoveOldFiles
     
    #>


    [CmdletBinding(
        DefaultParameterSetName = "Path"#,
        #SupportsShouldProcess = $True # cheating, tho... :P
    )]

    Param(
        [Parameter(
            Mandatory = $True,
            ParameterSetName = "Path",
            Position = 0,
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True)]
            [Alias('FilePath', 'FullName', 'Name')]
            [String[]] $Path,

        [Parameter(
            Mandatory = $True,
            ParameterSetName = "LiteralPath",
            Position = 0,
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True)]
            # Can't declare multiple aliases with the same name even in different parameter sets..
            #[Alias('FilePath', 'FullName', 'Name')]
            [String[]] $LiteralPath,

        [Switch] $Recurse,
        [Decimal] $Millisecond = 0,
        [Decimal] $Second = 0,
        [Decimal] $Minute = 0,
        [Decimal] $Hour = 0,
        [Decimal] $Day = 0,
        [Decimal] $Week = 0,
        [Decimal] $Month = 0,
        [Switch] $WhatIf = $False,
        [Regex] $NameRegexMatch = '.*')
        #[Switch] $RemoveEmptyDirectories) # to do...

   

    Begin {

        $VerbosePreference = "Continue"

        [DateTime] $Script:Now = Get-Date
        Write-Verbose "Recorded 'now' time as: $($Now.ToString('yyyy\-MM\-dd HH\:mm\:ss'))"

        <#
        Dead end?
 
        # Use to avoid infinite loops, use the ProviderPath property, store it and do not process
        # if it's already been processed. Lookups are fast.
 
        [HashTable] $Script:AlreadyProcessed = @{}
        #>


        [Decimal] $Script:InnerProcessPathCounter = 0
        [Decimal] $Script:DeleteCount = 0
        [Bool] $FirstLevelDirectoryListing = $True

        <# Abandoned.
        [HashTable] $Script:RecurseSplat = @{}
        if ($Recurse) {
            $Script:RecurseSplat = @{
                Recurse = $True
            }
        }#>


        [HashTable] $Script:WhatIfSplat = @{}
        if ($WhatIf) {
            $Script:WhatIfSplat = @{
                WhatIf = $True
            }
        }

        # Months is a special case since it's not fixed like the rest.
        # Figure it'll make it awesomerer to support that as well.
        [Decimal] $TotalMillisecondsBack = 0
        if ($Month -gt 0) {
            $LastMontshMilliseconds = ($Now - $Now.AddMonths(( -1 * $Month ))).TotalMilliseconds
        }
        else {
            $LastMontshMilliseconds = 0
        }

        $TotalMillisecondsBack += $LastMontshMilliseconds
        $TotalMillisecondsBack += $Millisecond
        $TotalMillisecondsBack += $Second * 1000
        $TotalMillisecondsBack += $Minute * 60 * 1000
        $TotalMillisecondsBack += $Hour * 60 * 60 * 1000
        $TotalMillisecondsBack += $Day * 24 * 60 * 60 * 1000
        $TotalMillisecondsBack += $Week * 7 * 24 * 60 * 60 * 1000

        if ($TotalMillisecondsBack -eq 0) {
            Write-Error -Message ("Error. You need to specify how long back in time to filter on. All files that " + `
                "were modified before the specified number of milliseconds, seconds, minutes, hours, days, weeks " + `
                "and months will be attempted deleted. If you add multiple, such as -Minute 1 and -Second 30, it" + `
                " will be added up to 90 seconds / 1.5 minutes.") `
                -ErrorAction Stop
        }
        Write-Verbose "Total milliseconds: $TotalMillisecondsBack.
$Month months.
$Week weeks.
$Day days.
$Hour hours.
$Minute minutes.
$Second seconds.
$Millisecond milliseconds."

        
        # This seems to do some of the trick.. optimized variable and stuff.
        $Script:InGetChildItem = $False

        function InnerProcessPath {               

            Param([String] $Path)
                
            [Bool] $FirstLevelDirectoryListing = $True
                
            ++$Script:InnerProcessPathCounter

            # Hm, this had unexpected side effects when considering multiple path support.
            # My life would have been a lot easier with a single path supported here.
            #if ($Script:AlreadyProcessed.ContainsKey($Path)) {
            # continue
            #}

            Write-Verbose "in InnerProcessPath processing '$Path'."
            if (Test-Path -LiteralPath $Path -PathType Container) {

                if ($Recurse -or ($FirstLevelDirectoryListing -and $InGetChildItem -eq $False)) {

                    if ($FirstLevelDirectoryListing) {
                        Write-Verbose "Processing first level directory with path: '$Path'."
                        $FirstLevelDirectoryListing = $False
                    }
                        
                        
                    Get-ChildItem -LiteralPath $Path | ForEach-Object {
                        $Script:InGetChildItem = $True
                        InnerProcessPath -Path $_.FullName
                    }
                    $Script:InGetChildItem = $False
                }

                else {
                    Write-Verbose "-Recurse parameter not specified, so directory '$Path' is skipped."
                }

            }

            elseif (Test-Path -LiteralPath $Path -PathType Leaf) {
                    
                $Item = Get-Item -LiteralPath $Path
                    
                if (($Script:Now - $Item.LastWriteTime).TotalMilliseconds -gt $TotalMillisecondsBack) {
                    
                    if (-not $WhatIf -and $Item.Name -match $NameRegexMatch) {
                        Write-Verbose "Removing file '$($Item.FullName
                            )' because it was modified more than $($TotalMillisecondsBack
                            ) ms ago and matches the specified name regex: '$NameRegexMatch' (default '.*')."


                        ++$Script:DeleteCount
                        Remove-Item -LiteralPath $Item.FullName @Script:WhatIfSplat
                    }

                    elseif ($Item.Name -match $NameRegexMatch) {
                        Write-Verbose ("Would have removed '$($Item.FullName
                            ) because it was modified more than $TotalMillisecondsBack ms ago and matches "
 + `
                            "the specified name regex: '$NameRegexMatch' (default '.*'), without -WhatIf in use.")

                        ++$Script:DeleteCount
                    }
                    elseif ($WhatIf) {
                        Write-Verbose "Ignoring. -WhatIf in use and file '$($Item.FullName
                            )' does not match the specified name regex: '$NameRegexMatch' (default '.*')."

                    }
                    else {
                        Write-Verbose ("Ignoring. File '$($Item.FullName)' does not match the " + `
                            "specified name regex: '$NameRegexMatch' (default '.*').")
                    }
                }
                else {
                    Write-Verbose "Ignoring file '$($Item.FullName)' because it was modified less than $TotalMillisecondsBack ms ago."
                }

            }
            else {
                Write-Warning -Message "Path '$Path' doesn't exist. Skipping."
            }

            # Abandoned.
            #$Script:AlreadyProcessed[$Path] = $True

        } # end of function

    }

    Process {

        if ($PSCmdlet.ParameterSetName -eq "Path") {
            $PathsToProcess = @($Path)
        }
        elseif ($PSCmdlet.ParameterSetName -eq "LiteralPath") {
            $PathsToProcess = @($LiteralPath)
        }

        foreach ($TempPath in $PathsToProcess) {
            
            $PathBeforeResolvePath = $TempPath
            
            Write-Verbose "Processing path '$PathBeforeResolvePath'."
            
            if ($PSCmdlet.ParameterSetName -eq "Path") {
                $TempPath = @(Resolve-Path -Path $TempPath |
                    Select-Object -ExpandProperty ProviderPath)
            }
            
            else {
                $TempPath = @(Resolve-Path -LiteralPath $TempPath |
                    Select-Object -ExpandProperty ProviderPath)
            }

            if ($null -eq $TempPath) {
                Write-Warning -Message "Couldn't resolve path '$PathBeforeResolvePath' or it doesn't exist. Skipped."
                continue
            }

            #$Script:FirstLevelDirectoryListing = $True
            foreach ($TempTempPath in $TempPath) {
                Write-Verbose "In foreach `$TempTempPath processing path '$TempTempPath'."
                $Script:InGetChildItem = $False
                InnerProcessPath -Path $TempTempPath
            }

        } # end of foreach $TempPath in $PathsToProcess

    }

    End {

        Write-Verbose "Total processed path count was: $Script:InnerProcessPathCounter"

        if ($WhatIf) {
            Write-Verbose "Would have attempted to delete $Script:DeleteCount files without -WhatIf in use."
        }
        else {
            Write-Verbose "Attempted to delete $Script:DeleteCount files."
        }

    }

}