classes/New-LogObject.ps1

function New-LogObject {
    #####################
    # LogObject Class #
    #####################
    # Properties
    $LogObject = [PSCustomObject]@{
        'Logfile' = ''
        'Options' = ''
        'Status' = ''
        'Metadata' = ''
        'PrivateMethods'= [scriptblock]{
            ##########################
            # Object Private Methods
            ##########################
            function Rotate-Main {
                try {
                    # Rotate main file
                    # E.g. D:/console.log -> D:/console.log.1
                    if ($copy) {
                        Write-Verbose "Copying $my_fullname to $my_previous_fullname"

                        if (Test-Path $my_previous_fullname) {
                            # File exists.
                            Write-Verbose "Error creating output file $my_previous_fullname`: File exists"
                        }else {
                            if (!$WhatIf) {
                                Copy-Item $my_fullname $my_previous_fullname -ErrorAction Stop
                            }
                            if ($copytruncate) {
                                Write-Verbose "Truncating $my_fullname"
                                if (!$WhatIf) {
                                    # Truncate the file
                                    [IO.File]::WriteAllText($my_fullname, '')
                                }
                            }else {
                                Write-Verbose "Not truncating $my_fullname"
                            }
                            return $true
                        }
                    }else {
                        Write-Verbose "Renaming $my_fullname to $my_previous_fullname"
                        if (Test-Path $my_previous_fullname) {
                            # File exists.
                            Write-Verbose "Error creating output file $my_previous_fullname`: File exists"
                        }else {
                            if (!$WhatIf) {
                                Move-Item $my_fullname $my_previous_fullname -Force
                            }
                            if ($create) {
                                Write-Verbose "Creating new log file $my_fullname"
                                if (!$WhatIf) {
                                    $newitem = New-Item $my_fullname -ItemType File | Out-Null
                                    if ($newitem) {

                                    }
                                }
                            }
                            return $true
                        }
                    }
                }catch {
                    throw
                }

                $false
            }

            function Rotate-Previous-Files-Incremental
            {
                # This function is only used for incremental index extensions, E.g. console.log.1 -> console.log.2. It is not used for date extensions.
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [int]$rotate_compressed_files
                ,
                    [Parameter(Mandatory=$True,Position=4)]
                    [int]$max_index = 0
                )

                [Regex]$regex = if ($rotate_compressed_files) {
                                    $my_previous_compressed_captures_regex
                                }else {
                                    $my_previous_noncompressed_captures_regex
                                }
                $previous_name_prototype =  if ($compress) {
                                                $my_previous_compressed_name_prototype
                                            }else {
                                                $my_previous_name_prototype
                                            }
                $match = $regex.Match($previous_name_prototype)
                if ($match.success) {
                    $prefix = $match.Groups['prefix'].Value
                    $suffix = $match.Groups['suffix'].Value -as [int]
                    $extension = if ($match.Groups['extension']) { $match.Groups['extension'].Value } else { '' }
                    $compressextension = if ($match.Groups['compressextension']) { $match.Groups['compressextension'].Value } else { '' }

                    if ($suffix) {
                        $SLASH = [IO.Path]::DirectorySeparatorChar
                        foreach ($i in @($max_index..0)) {
                            # Construct filenames with their index, and extension if provided
                            # E.g. D:\console.log.5 or D:\console.log.5.7z
                            $source_fullName = Join-Path $my_previous_directory "$prefix.$i$extension$compressextension"
                            # E.g. D:\console.log.6 or D:\console.log.6.7z
                            $destination_fullName = Join-Path $my_previous_directory "$prefix.$($i+1)$extension$compressextension"

                            # Rename old logs
                            Write-Verbose "Renaming $source_fullName to $destination_fullName (rotatecount $rotate, logstart $start, i $i)"
                            if ($WhatIf) { continue }
                            if (Test-Path $source_fullName) {
                                try {
                                    Move-Item -Path $source_fullName -Destination $destination_fullName -Force
                                }catch {
                                    throw
                                }
                            }else {
                                Write-Verbose "Old log $source_fullName does not exist."
                            }
                        }
                    }
                }
            }

            function Rename-File-Within-Compressed-Archive {
                # TODO: Not using this function for now, because this always recreates an archive, dumping a lot to the disk.

                # Only needed for 7z for now
                if ($compresscmd -notmatch '7za?') {
                    return
                }

                Get-Files $my_previous_compressed_regex $my_previous_directory | & {
                    begin {
                    }process {
                        $directory =$_.Directory.FullName
                        $fullName = $_.FullName
                        $baseName = $_.BaseName

                        $params = @( 'rn', $fullName, '*', $baseName )
                        try {
                            Write-Verbose "Rename log inside compressed archive $fullName to $baseName"
                            if ($WhatIf) {
                                continue
                            }

                            $scriptblock = {
                                param ($cd, $cmd, [string[]]$params)
                                Set-Location $cd
                                Write-Output "cd: $cd"
                                Write-Output "cmd: $cmd"
                                Write-Output "params: "
                                $params | Out-String | ForEach-Object { Write-Output $_.Trim() }
                                & $cmd $params
                            }
                            $job = Start-Job -ScriptBlock $scriptblock -ArgumentList $directory,$compresscmd,$params -ErrorAction Stop
                            $output = Receive-Job -Job $job -Wait -ErrorAction Stop
                            if ($job.State -eq 'Failed') {
                                throw "Renaming failed because: $($job.ChildJobs[0].JobStateInfo.Reason.Message)"
                            }else {
                                Write-Verbose "Renaming log within compressed archive $fullName successful. Output: `n$output"
                            }
                        }catch {
                            #Write-Verbose "Renaming failed for log within compressed archive $fullName ."
                            Write-Verbose "Renaming log within compressed archive $fullName failed."
                            Write-Error -ErrorRecord $_ -ErrorAction Continue
                        }
                    }
                }
            }

            function Compress-File {
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [string]$compressed_fullname
                ,
                    [Parameter(Mandatory=$True,Position=1)]
                    [string]$filter
                )

                $compressed = $false

                # E.g. 7z.exe a -t7z D:\console.log.7z D:\console.log
                # E.g. gzip.exe D:\console.log
                $compressoptions = @( $compressoptions -split '\s' | Where-Object { $_.Trim() } )

                $params = if ($compresscmd -match '7z') {
                            $compressoptions + $compressed_fullname + $filter
                        }else {
                            $compressoptions + $filter
                        }
                # Remove empty parameters
                $params = $params | Where-Object { $_ }

                try {
                    Write-Verbose "Compressing log with: $compresscmd"
                    Write-Verbose "Compress command line: $compresscmd $( $params -join ' ' )"
                    if ($WhatIf) {
                        return
                    }

                    $output = & $compresscmd $params
                    if (Test-Path $compressed_fullname) {
                        Write-Verbose "Compression successful. Output: `n$output"

                        $compressed = $true
                    }else {
                        Write-Verbose "Compression failed. Output: `n$output"
                        throw "Compressed file was not created."
                    }

                    # TODO: Not using jobs for now, because they are slow.
                    <#
                    $scriptblock = {
                        param ($cd, $cmd, [string[]]$params)
                        Set-Location $cd
                        #Write-Output "cd: $cd"
                        #Write-Output "cmd: $cmd"
                        #Write-Output "params: "
                        #$params | Out-String | ForEach-Object { Write-Output $_.Trim() }
                        & $cmd $params
                    }
                    $job = Start-Job -ScriptBlock $scriptblock -ArgumentList $logfile.Directory.FullName,$compresscmd,$params -ErrorAction Stop
                    $output = Receive-Job -Job $job -Wait -ErrorAction Stop

                    if ($job.State -eq 'Failed') {
                        throw "Compression failed because: $($job.ChildJobs[0].JobStateInfo.Reason.Message)"
                    }else {
                        if (Test-Path $compressed_fullname) {
                            Write-Verbose "Compression successful. Output: `n$output"

                            $compressed = $true
                        }else {
                            Write-Verbose "Compression failed. Output: `n$output"
                            throw "Compressed file was not created."
                        }
                    }
                    #>

                }catch {
                    Write-Verbose "Compression failed."
                    throw
                }

                # Remove the previous file
                if ($compressed) {
                    if (Test-Path $filter) {
                        Purge-File $filter
                        #Write-Verbose "Removed $filter"
                    }
                }
            }

            function Uncompress-File {
                # TODO: Not using uncompress because we're not doing mail for now.
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [string]$compressed_fullname
                )

                $uncompressed = $false

                # E.g. Extract as a file: 7z.exe x -t7z D:\console.log.7z
                # E.g. Extract to stdout: 7z.exe x -so D:\console.log.7z
                $uncompressoptions = $uncompressoptions.Split(' ') | Where-Object { $_.Trim() }
                $params = $uncompressoptions + $compressed_fullname

                try {
                    $stdout = & $compresscmd $params
                    if ($stdout) {
                        Write-Verbose "Uncompression successful. Output: `n$output"

                        # Store the file
                        $stdout | Out-File -Encoding utf8

                        $uncompressed = $true
                    }else {
                        Write-Verbose "Uncompression failed. Output: `n$output"
                        throw "Uncompressed file was not created."
                    }

                    # TODO: Not using jobs for now, because they are slow.
                    <#
                    $scriptblock = {
                        param ($cd, $cmd, [string[]]$params)
                        Set-Location $cd
                        #Write-Output "cd: $cd"
                        #Write-Output "cmd: $cmd"
                        #Write-Output "params: "
                        #$params | Out-String | ForEach-Object { Write-Verbose $_.Trim() }
                        & $cmd $params
                    }
                    $job = Start-Job -ScriptBlock $scriptblock -ArgumentList (Get-Item $compressed_fullname).Directory.FullName,$uncompresscmd,$params -ErrorAction Stop
                    $output = Receive-Job -Job $job -Wait -ErrorAction Stop
                    if ($job.State -eq 'Failed') {
                        throw "Compression failed because: $($job.ChildJobs[0].JobStateInfo.Reason.Message)"
                    }else {
                        if (Test-Path $compressed_fullname) {
                            Write-Verbose "Uncompression successful. Output: `n$output"
                            $uncompressed = $true
                        }else {
                            Write-Verbose "Uncompression failed. Output: `n$output"
                            throw "Uncompressed file was not created."
                        }
                    }
                    #>

                }catch {
                    Write-Verbose "Uncompression failed."
                    throw
                }

                # Remove the compressed file
                if ($uncompressed) {
                    if (Test-Path $compressed_fullname) {
                        Remove-Item $compressed_fullname
                        Write-Verbose "Removed $compressed_fullname"
                    }
                }
            }

            function Notify-Purge {
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [string]$file_fullname
                )

                if ( !(Test-Path $file_fullname) ) {
                    Write-Verbose "log $file_fullname doesn't exist -- won't try to dispose of it "
                }
            }

            function Purge-File {
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [string]$file_fullname
                )

                if (Test-Path $file_fullname) {
                    Write-Verbose "Removing old log $file_fullname"

                    # Run preremove script
                    if ($preremove) {
                        Write-Verbose "Running preremove script"
                        try {
                            Start-Script $preremove $file_fullname -ErrorAction $CallerEA
                        }catch {
                            Write-Error "Failed to run preremove script." -ErrorAction Continue
                            throw
                        }
                    }

                    # Delete file
                    if (!$WhatIf) {
                        Remove-Item $file_fullname
                    }else {
                        # For debugging to simulate deleted file
                        $debug_my_prevfilespurged_fullnames.Add($file_fullname) | Out-Null
                    }
                }
            }

            function Remove-Old-Files {
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [AllowNull()]
                    [Array]$files
                ,
                    [Parameter(Mandatory=$True,Position=1)]
                    [ValidateRange(0, [int]::MaxValue)]
                    [int]$keep_count
                ,
                    [Parameter(Mandatory=$True,Position=2)]
                    [int]$oldest_is_first_when_name_sorted
                )

                $files_count =  if ($files) {
                                    $files.Count
                                }else {
                                    0
                                }
                if ($files_count) {
                    if ($keep_count -ge $files_count) {
                        # If keeping 365 copies, and I only have 5.
                        # If keeping 5 copies, and I have 5
                        Write-Verbose "No more old files to remove $($_.FullName)"
                        return
                    }

                    $oldfiles_count = $files_count - $keep_count
                    $oldfiles = if ($oldest_is_first_when_name_sorted) {
                        # Datetime. Exclude the last x items, when sorted by name ascending.
                        #$files | Sort-Object -Property Name | Select-Object -SkipLast $keep_count
                        $files | Sort-Object -Property Name | Select-Object -First $oldfiles_count
                    }else {
                        # Descending index. Exclude the last x items, when sorted by name descending.
                        #$files | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) } -Descending | Select-Object -SkipLast $keep_count
                        $files | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) } -Descending | Select-Object -First $oldfiles_count
                    }
                    $oldfiles | ForEach-Object {
                        Write-Verbose "Removing $($_.FullName)"
                        Purge-File $_.FullName
                    }
                }
            }

        }
        'HelperMethods' = [scriptblock]{
            ##########################
            # Object Private Helper Methods
            ##########################
            function Get-Files {
                param (
                    [Parameter(Mandatory=$True,Position=0)]
                    [string]$regex,
                    [Parameter(Mandatory=$True,Position=1)]
                    [string]$directory
                )
                Get-ChildItem $directory | Where-Object { $_.Name -match $regex }
            }
        }
    }
    # Methods
    $LogObject | Add-Member -Name 'New' -MemberType ScriptMethod -Value {
        <# Constructs a Log Object (hashtable) representing a rotatable log. File, options and metadata (keys) mapped to their data.
        @{
            'Logfile' = $logfile
            'Options' = @{
                'compress' = $true;
                'rotate' = '5';
                ...
            }
            'Status' = @{
                'preprerotate' = $false
                'prerotate' = $false
                'rotate' = $false
                'postrotate' = $false
                'postpostrotate' = $false
                'rotation_datetime' = (Get-Date).ToLocalTime()
            }
            'Metadata' = @{
                my_name = 'console.log.';
                $my_extension = '.log';
                $my_stem = '.log';
                ...
            }
        }
        #>

        param ([System.IO.FileInfo]$logfile, [hashtable]$options, [string]$lastRotationDate)

        # Unpack the block's options into variables
        $options.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $options[$_]
        }

        #$VerbosePreference = $oldVerbosePreference

        # Builds a Constructed Log Object, for log files that should be rotated
        if ($logfile) {
            # E.g. '\' for WinNT, '/' for nix
            $SLASH = [IO.Path]::DirectorySeparatorChar

            # E.g. 'console.log' and 'D:\console.log'
            $my_name = $logfile.Name
            $my_fullname = $logfile.FullName

            # E.g. '.log'
            $my_extension = $logfile.Extension

            # Are we preserving the extension?
            $_preserve_extension = $extension -and $my_extension -and ($my_extension -eq $extension)
            Write-Verbose (& { if (!$_preserve_extension) {  "Not preserving extension." } else { "Preserving extension: $extension" } })

            # If we're preserving extension (which can consist of multiple .), we need the stam of the filename.
            # E.g. 'console'. It's the same as $_.BaseName
            $my_stem =  if ($extension) {
                            # E.g. '(.*)\.log' will capture 'console', when extension is '.log'
                            $my_stem_regex = [Regex]"(.*)$( [Regex]::Escape($extension) )$"
                            $matches = $my_stem_regex.Match($my_name)
                            $matches.Groups[1].Value
                        }else {
                            $logfile.BaseName
                        }

            # E.g. 'console\.log'
            $my_name_regex = [Regex]::Escape($my_name)

            # E.g. '\.log'
            $extension_regex = [Regex]::Escape($extension)

            # E.g. 'console'
            $my_stem_regex = [Regex]::Escape($my_stem)

            # Get current directory. E.g. 'D:\data'
            $my_directory = $logfile.Directory.FullName

            # Validate our olddir is a directory, and resolve it to an absolute path
            if ($olddir) {
                # Try relative location, then try absolute

                if ([System.IO.Path]::IsPathRooted($olddir)) {
                    # Absolute path
                }else {
                    # Relative path. Check for existance of an olddir in the same directory as the log file
                    $olddir = Join-Path $my_directory $olddir
                }
                if ( !(Test-Path $olddir -PathType Container) ) {
                    throw "Invalid olddir: $olddir. Not using olddir. Skipping log $($logfile.FullName)!"
                }
            }

            # Get previous directory. E.g. D:\data or D:\data\olddir
            $my_previous_directory = if ($olddir) { $olddir } else { $my_directory }

            # Check directories' permissions, skip over if insufficient permissions.
            foreach ($dir in $my_directory,$my_previous_directory) {
                try {
                    $_outfile = Join-Path $dir ".test$(Get-Date -Format 'yyyyMMdd')"
                    [io.file]::OpenWrite($_outfile).close()
                    Remove-Item $_outfile
                }catch {
                    throw "Insufficient permissions on $dir. Ensure the user $($env:username) has read and write permissions on the directory."
                }
            }

            # Build our glob patterns
            $my_date = ''
            $my_date_regex = ''
            if ($dateext -or !$dateext) {
                # Generate the date extension

                # Trim the strftime datetime format string
                $_dateformat_tmp = $dateformat
                $dateformat = $dateformat.Trim()
                Write-Verbose "Converted dateformat from '$_dateformat_tmp' to '$dateformat'"

                # Convert the strftime datetime format string (specifiers: %Y, %m, and %d) to a .NET datetime format string (specifiers: yyyy, mm, dd). Then use it to get the current datetime as a string
                # E.g. '2017-12-25'
                #$_format = $dateformat.replace('%Y', 'yyyy').replace('%m', 'MM').replace('%d', 'dd')
                #$my_date = Get-Date -Format $_format

                # Replace specifier %s with unix epoch first in the strftime datetime format string. Then use it to get the current datetime as a string.
                # E.g. '2017-12-25'
                $_unix_epoch = [Math]::Floor([decimal](Get-Date (Get-Date).ToUniversalTime() -uformat "%s"))
                $_uformat = $dateformat.replace('%s', $_unix_epoch)
                $my_date = Get-Date -UFormat $_uformat
                Write-Verbose "Determined date extension to be '$my_date'"

                # Determine glob
                # E.g. [0-9]{4}-[0-9]{2}-[0-9]{2}
                $_unix_epoch_length = $_unix_epoch.ToString().Length
                $my_date_regex = $dateformat.Replace('%Y', '[0-9]{4}').Replace('%m', '[0-9]{2}').Replace('%d', '[0-9]{2}').Replace('%s', "[0-9]{$_unix_epoch_length}")
                Write-Verbose "Determined date glob pattern: '$my_date_regex'"
            }

            # Build our previous name and previous fullname
            $my_previous_name = & {
                if ($_preserve_extension) {
                    if ($dateext) {
                        # E.g. 'console-2017-11-20.log'
                        "$my_stem$my_date$extension"
                    }else {
                        # E.g. 'console.1.log'
                        "$my_stem.$start$extension"
                    }
                }else {
                    if ($dateext) {
                        # E.g. 'console.log-2017-11-20'
                        "$my_name$my_date"
                    }else {
                        # E.g. 'console.log.1'
                        "$my_name.$start"
                    }
                }
            }
            $my_previous_fullname = Join-Path $my_previous_directory $my_previous_name

            # Determine the to-be-rotated log's compressed file name, if we are going to
            # E.g. 'D:\console.log.1.7z'
            $my_previous_compressed_fullname =  if ($compress) {
                                                    "$my_previous_fullname$compressext"
                                                } else {
                                                    ''
                                                }

            # Build prototype names
            # E.g. 'console.log.1'
            # E.g. 'console.log.1.7z'
            $my_previous_name_prototype = $my_previous_name
            $my_previous_compressed_name_prototype =    if ($compress) {
                                                            "$my_previous_name$compressext"
                                                        }

            $my_index_regex = "\d{1,$( $rotate.ToString().Length )}";
            $my_previous_noncompressed_regex = if ($_preserve_extension) {
                                                    # E.g. '^console\-[0-9]{4}-[0-9]{2}-[0-9]{2}\.log$' or '^console\.\d{1,2}\.log$'
                                                    if ($dateext) {
                                                        "^$my_stem_regex$my_date_regex$extension_regex$"
                                                    }else {
                                                        "^$my_stem_regex\.$my_index_regex$extension_regex$"
                                                    }
                                                }else {
                                                    # E.g. '^console\.log\-[0-9]{4}-[0-9]{2}-[0-9]{2}$' or '^console\.log\.\d{1,2}$'
                                                    if ($dateext) {
                                                        "^$my_name_regex$my_date_regex$"
                                                    }else {
                                                        "^$my_name_regex\.$my_index_regex$"
                                                    }
                                                }
            # E.g. '^console\.log\.\d{1,2}\.7z$' or '^console\.log\-[0-9]{4}-[0-9]{2}-[0-9]{2}\.7z$'
            $my_previous_compressed_regex = ($my_previous_noncompressed_regex -replace ".$") + [Regex]::Escape($compressext) + "$"

            # The same as above, but with capture groups.
            $my_previous_noncompressed_captures_regex = if ($_preserve_extension) {
                                                            # E.g. '^(?<prefix>console)(?<suffix>\-[0-9]{4}-[0-9]{2}-[0-9]{2})(?<extension>\.log)$' or '^(?<prefix>console)\.(?<suffix>\d{1,2})(?<extension>\.log)$'
                                                            if ($dateext) {
                                                                "^(?<prefix>$my_stem_regex)(?<suffix>$my_date_regex)(?<extension>$extension_regex)$"
                                                            }else {
                                                                "^(?<prefix>$my_stem_regex)\.(?<suffix>$my_index_regex)(?<extension>$extension_regex)$"
                                                            }
                                                        }else {
                                                            # E.g. '^(?<prefix>console\.log)(?<suffix>\-[0-9]{4}-[0-9]{2}-[0-9]{2})$' or '^(?<prefix>console\.log)\.(?<suffix>\d{1,2})$'
                                                            if ($dateext) {
                                                                "^(?<prefix>$my_name_regex)(?<suffix>$my_date_regex)$"
                                                            }else {
                                                                "^(?<prefix>$my_name_regex)\.(?<suffix>$my_index_regex)$"
                                                            }
                                                        }
            # E.g. '^(?<prefix>console\.log)\.(?<suffix>\d{1,2})(?<compressextension>\.7z)$' or '^(?<prefix>console\.log)(?<suffix>\-[0-9]{4}-[0-9]{2}-[0-9]{2})(?<compressextension>\.7z)$'
            $my_previous_compressed_captures_regex = ($my_previous_noncompressed_captures_regex -replace ".$") + "(?<compressextension>$( [Regex]::Escape($compressext) ))$"

            # Get all my existing files
            $my_prevfiles = if ($compress) {
                                # Get all my existing non-compressed files in this folder. E.g. filter by '*.7z'
                                Get-ChildItem $my_previous_directory | Where-Object { $_.Name -match $my_previous_compressed_regex }
                            }else {
                                # Get all my existing compressed files in this folder. E.g. 'console.log.x.7z', where x is a number
                                Get-ChildItem $my_previous_directory | Where-Object { $_.Name -match $my_previous_noncompressed_regex }
                            }

            # The expired file name. Only used for non-date extension.
            $my_expired_fullName =  if ($_preserve_extension) {
                                        if ($compress) {
                                            # E.g. 'D:\console.6.log.7z'
                                            Join-Path $my_previous_directory "$my_stem.$($start+$rotate)$extension$compressext"
                                        }else {
                                            # E.g. 'D:\console.6.log'
                                            Join-Path $my_previous_directory "$my_stem.$($start+$rotate)$extension"
                                        }
                                    }else {
                                        if ($compress) {
                                            # E.g. 'D:\console.log.6.7z'
                                            Join-Path  $my_previous_directory "$my_name.$($start+$rotate)$compressext"
                                        }else {
                                            # E.g. 'D:\console.log.6'
                                            Join-Path $my_previous_directory "$my_name.$($start+$rotate)"
                                        }
                                    }


            # Rotate? ALL CONDITIONS BELOW
            $should_rotate = & {

                # If forced, no processing of rotation conditions needed. Go ahead and rotate.
                if ($force) {
                    return $true
                }

                # If never rotated before, go ahead
                #if (!$lastRotationDate) {
                # return $true
                #}

                # Don't rotate if log file size is 0, and we specified to not rotate empty files.
                if (!$ifempty) {
                    $my_size = $logfile.Length
                    if (!$my_size) {
                        Write-Verbose "Will not rotate log: $my_name. File size is 0."
                        return $false
                    }
                }

                # Don't rotate if my size is smaller than size threshold
                if ($size) {
                    $my_size = ($logfile | Measure-Object -Property Length -Sum -Average -Maximum -Minimum).Sum
                    if ($my_size -le (Get-Size-Bytes $size)) {
                        Write-Verbose "Will not rotate log: $my_name. File's size ($my_size) is less than defined ($size)."
                        return $false
                    }
                }

                # Don't rotate if we haven't met time thresholds by daily / weekly / monthy / yearly options.
                # If minsize specified along with time thresholds, don't rotate if either time or minsize thresholds are unmet.
                if ($daily -or $weekly -or $monthly -or $yearly) {
                    $time_interval_over = & {
                        # If it's our first time, considered to have met the time threshold
                        if (!$lastRotationDate) {
                            return $true
                        }

                        $_now_dt = (Get-Date).ToLocalTime()
                        # Not using CreationTime, but using state file now.
                        #$_my_newest_file = $my_prevfiles | Sort-Object -Property CreationTime -Descending | Select-Object -First 1
                        #$lastRotationDate = $_my_newest_file.CreationTime.ToLocalTime()
                        $_last_dt = Get-Date -Date $lastRotationDate
                        $_days_ago = (New-TimeSpan -Start $_last_dt -End $_now_dt ).Days
                        if ($daily) {
                            if ($_days_ago -ge 1) {
                                Write-Verbose "Time interval over. Last rotation occured a day or more ago. Last rotation: $($_last_dt.ToString('s'))"
                                return $true
                            }else {
                                Write-Verbose "Time interval not over. Last rotation occured less than a day ago. Last rotation: $($_last_dt.ToString('s'))"
                            }
                        }elseif ($weekly) {
                            if ($_days_ago -ge 7) {
                                Write-Verbose "Time interval over. Last rotation occured a week or more ago. Last rotation: $($_last_dt.ToString('s'))"
                                return $true
                            }elseif ( ($_days_ago -lt 7) -and ($_now_dt.DayOfWeek.value__ -lt $_last_dt.DayOfWeek.value__) ) {
                                Write-Verbose "Time interval over. Current weekday is less than weekday of last rotation."
                                return $true
                            }else {
                                Write-Verbose "Time interval not over. Last rotation occured less than a week ago. Last rotation: $($_last_dt.ToString('s'))"
                            }
                        }elseif ($monthly) {
                            if ($_now_dt.Month -gt $_last_dt.Month) {
                                Write-Verbose "Time interval over. This is the first time logrotate is run this month. "
                                return $true
                            }elseif ($_now_dt.Year -gt $_last_dt.Year) {
                                # The case where the current year is a new year
                                Write-Verbose "Time interval over. Last rotation occured last year. Last rotation: $($_last_dt.ToString('s'))"
                                return $true
                            }else {
                                Write-Verbose "Time interval not over. Last rotation already occured this month. Last rotation: $($_last_dt.ToString('s'))"
                            }
                        }elseif ($yearly) {
                            if ($_now_dt.Year -ne $_last_dt.Year) {
                                Write-Verbose "Time interval over. Last rotation occured on a different year as this year. Last rotation: $($_last_dt.ToString('s'))"
                                return $true
                            }else {
                                Write-Verbose "Time interval not over. Last rotation already occured this year. Last rotation: $($_last_dt.ToString('s'))"
                            }
                        }
                        $false
                    }
                    if ($time_interval_over) {
                        # If minsize is specified, both time and minsize thresholds will be considered
                        if ($minsize) {
                            $my_size = ($logfile | Measure-Object -Property Length -Sum -Average -Maximum -Minimum).Sum
                            Write-Verbose "my_size: $my_size"
                            if ($my_size -ge $minsize) {
                                # Minsize threshold met
                                Write-Verbose "Will rotate log: $my_name. Time interval over, and minsize met. File's size ($my_size) is less than minsize ($minsize)"
                                return $true
                            }else {
                                # Minsize threshold unmet
                                Write-Verbose "Will not rotate log: $my_name. Time interval over, but minsize not met. File's size ($my_size) is less than defined ($minsize)."
                                return $false
                            }
                        }else {
                            # No minsize specified. Only time threshold met.
                        }

                        # Time threshold met. Will rotate.
                        return $true
                    }else {
                        # Haven't met time threshold. Don't rotate.
                        return $false
                    }
                }

                # True by default. No conditions stopped us from moving on.
                $true
            }

            # Assign properties to the Log Object
            if ($should_rotate) {

                $_logObject = $LogObject.psobject.copy()
                $_logObject.Logfile = $logfile;
                $_logObject.Options = $options;
                $_logObject.Status = @{
                    'preprerotate' = $false
                    'prerotate' = $false
                    'rotate' = $false
                    'postrotate' = $false
                    'postpostrotate' = $false
                    'rotation_datetime' = (Get-Date).ToLocalTime()
                }
                $_logObject.Metadata = @{
                    'my_name' = $my_name
                    'my_extension' = $my_extension
                    'my_stem' = $my_stem
                    'my_directory' = $my_directory
                    'my_previous_directory' = $my_previous_directory
                    'my_previous_name' = $my_previous_name
                    'my_previous_fullname' = $my_previous_fullname
                    'my_date' = $my_date

                    'my_name_regex' = $my_name_regex
                    'my_date_regex' = $my_date_regex
                    'my_previous_noncompressed_regex' = $my_previous_noncompressed_regex
                    'my_previous_compressed_regex' = $my_previous_compressed_regex
                    'my_previous_noncompressed_captures_regex' = $my_previous_noncompressed_captures_regex
                    'my_previous_compressed_captures_regex' = $my_previous_compressed_captures_regex
                    'my_previous_compressed_fullname' = $my_previous_compressed_fullname

                    'my_prevfiles' = $my_prevfiles

                    'my_previous_name_prototype' = $my_previous_name_prototype
                    'my_previous_compressed_name_prototype' = $my_previous_compressed_name_prototype

                    'my_expired_fullName' = $my_expired_fullName

                    # For debug mode
                    'debug_my_prevfilespurged_fullnames' = [System.Collections.ArrayList]@()

                    'my_fullname' = $my_fullname
                    'SLASH' = $SLASH
                }

                return $_logObject
            }
        }else {
            if ($nomissingok) {
                throw "Specified log $logfile is not a file."
            }
        }
        $null
    }
    $LogObject | Add-Member -Name 'PrePrerotate' -MemberType ScriptMethod -Value {
        # Unpack Object properties
        Set-Variable -Name 'logfile' -Value $this.Logfile
        $this.Options.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Options[$_]
        }
        $this.Metadata.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Metadata[$_]
        }

        # Unpack Object methods
        . $this.PrivateMethods
        . $this.HelperMethods

        Write-Verbose "rotating log $my_fullname, log->rotateCount is $rotate"
        Write-Verbose "date suffix '$my_date'"
        Write-Verbose "date glob pattern '$my_date_regex'"

        if (!$compress) {
            # Normal rotation without compression

            if (!$dateext) {
                # Rotate all previous files
                # D:\console.log.1.7z -> D:\console.log.2.7z
                Rotate-Previous-Files-Incremental 0 ($start+$rotate-1)

                Notify-Purge $my_expired_fullName

                $this.Status.preprerotate = $true
            }else {
                # Delete old logs
                if ($rotate -gt 0) {
                    Remove-Old-Files (Get-Files $my_previous_noncompressed_regex $my_previous_directory) $rotate 1
                }
                if (Test-Path $my_previous_fullname) {
                    Write-Verbose "Destination $my_previous_fullname already exists, skipping rotation"
                }else {
                    $this.Status.preprerotate = $true
                }
            }
        }elseif ($compress) {
            # Compress

            if (!$dateext) {

                if (!$delaycompress) {
                    # Rotate all previous compressed files
                    # D:\console.log.1.7z -> D:\console.log.2.7z
                    Rotate-Previous-Files-Incremental 1 ($start+$rotate-1)

                    # TODO: Not using for now. See the comments in function.
                    #Rename-File-Within-Compressed-Archive

                    Notify-Purge $my_expired_fullName

                    $this.Status.preprerotate = $true
                }else {
                    # Flag to indicate safe to rotate
                    $_skip_rotate = $false
                    if (Test-Path $my_previous_fullname) {
                        if (Test-Path $my_previous_compressed_fullname) {
                            # File exists.
                            Write-Verbose "Error creating output file $my_previous_compressed_fullname : file exists"
                            $_skip_rotate = $true
                        }else {
                            # Compress previous file, Remove compression source file
                            # D:\console.log.1 -> D:\console.log.1.7z
                            Compress-File $my_previous_compressed_fullname $my_previous_fullname
                        }
                    }

                    if ($_skip_rotate) {
                        Notify-Purge $my_expired_fullName
                    }else {
                        # Rotate all previous compressed files
                        # D:\console.log.1.7z -> D:\console.log.2.7z
                        Rotate-Previous-Files-Incremental 1 ($start+$rotate-1)

                        # Disabled for now, because this always recreates an archive, dumping a lot to the disk.
                        #Rename-File-Within-Compressed-Archive

                        Notify-Purge $my_expired_fullName

                        $this.Status.preprerotate = $true
                    }
                }
            }else {

                # Date extension
                if (!$delaycompress) {

                    # Delete old logs
                    if ($rotate -gt 0) {
                        Remove-Old-Files (Get-Files $my_previous_compressed_regex $my_previous_directory) $rotate 1
                    }

                    if (Test-Path $my_previous_compressed_fullname) {
                        # File exists.
                        Write-Verbose "Destination file $my_previous_compressed_fullname already exists, skipping rotation"
                    }else {
                        $this.Status.preprerotate = $true
                    }
                }else {
                    # Compress any uncompressed previous files
                    $_skip_rotate = $false
                    Get-Files $my_previous_noncompressed_regex $my_previous_directory | ForEach-Object {
                        # Skip over the rest of pipeline
                        if ($_skip_rotate) {
                            return
                        }

                        # E.g. console.log-2017-11-25
                        $_fullname = $_.FullName
                        # E.g. console.log-2017-11-25.7z
                        $_compressed_fullname = "$_fullname$compressext"

                        if ( Test-Path $_compressed_fullname ) {
                            Write-Verbose "Error creating output file $_compressed_fullname`: File exists"

                            $_skip_rotate = $true
                        }else {
                            # Compress previous file, Remove compression source file
                            # D:\console.log-2017-11-25 -> D:\console.log-2017-11-25.7z
                            Compress-File $_compressed_fullname $_fullname
                        }
                    }

                    if ($_skip_rotate) {
                        # Don't proceed any further
                    }else {
                        # Delete old logs
                        if ($rotate -gt 0) {
                            Remove-Old-Files (Get-Files $my_previous_compressed_regex $my_previous_directory) $rotate 1
                        }
                        if ( (Test-Path $my_previous_fullname) -or (Test-Path $my_previous_compressed_fullname) ) {
                            # File exists.
                            Write-Verbose "Destination file $my_previous_fullname already exists, skipping rotation"
                        }else {
                            $this.Status.preprerotate = $true
                        }
                    }
                }

            } # End if ($dateext)

        } # End if ($compress)

        # Removed this - Script will spit stdout on the Pipeline
        #$this.Status.preprerotate
    }
    $LogObject | Add-Member -Name 'Prerotate' -MemberType ScriptMethod -Value {
        # Unpack Object properties
        $prerotate = $this.Options['prerotate']
        $my_fullname = $this.Metadata['my_fullname']

        if ($prerotate) {
            Write-Verbose "Running prerotate script"
            try {
                Start-Script $prerotate $my_fullname -ErrorAction Stop
            }catch {
                Write-Error  "Failed to run prerotate script." -ErrorAction Continue
                throw
            }

            $this.Status.prerotate = $true
        }

        # Removed this - Script will spit stdout on the Pipeline
        #$this.Status.prerotate
    }
    $LogObject | Add-Member -Name 'RotateMainOnly' -MemberType ScriptMethod -Value {
        # Unpack Object properties
        Set-Variable -Name 'logfile' -Value $this.Logfile
        $this.Options.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Options[$_]
        }
        $this.Metadata.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Metadata[$_]
        }

        # Unpack Object methods
        . $this.PrivateMethods

        $this.Status.rotate = Rotate-Main
        # Removed this - Script will spit stdout on the Pipeline
        #$this.Status.rotate
    }
    $LogObject | Add-Member -Name 'Postrotate' -MemberType ScriptMethod -Value {
        # Unpack Object properties
        $postrotate = $this.Options['postrotate']
        $my_fullname = $this.Metadata['my_fullname']

        if ($postrotate) {
            Write-Verbose "Running postrotate script"
            try {
                Start-Script $postrotate $my_fullname -ErrorAction $CallerEA
            }catch {
                Write-Error "Failed to run postrotate script." -ErrorAction Continue
                throw
            }
            $this.Status.postrotate = $true
        }

        # Removed this - Script will spit stdout on the Pipeline
        #$this.Status.postrotate
    }
    $LogObject | Add-Member -Name 'PostPostRotate' -MemberType ScriptMethod -Value {
        # Unpack Object properties
        Set-Variable -Name 'logfile' -Value $this.Logfile
        $this.Options.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Options[$_]
        }
        $this.Metadata.Keys | ForEach-Object {
            Set-Variable -Name $_ -Value $this.Metadata[$_]
        }

        # Unpack Object methods
        . $this.PrivateMethods
        . $this.HelperMethods

        if (!$compress) {
            # Normal rotation without compression

            if (!$dateext) {
                # Non-dateext: we'll just purge a single file. At least that's how it works the actual logrotate.

                # Remove expired file
                Purge-File $my_expired_fullName

                $this.Status.postpostrotate = $true
            }else {
                # Remove expired files
                if ($rotate -gt 0) {
                    $keep_prev_count = $rotate
                    $prev_files = Get-Files $my_previous_noncompressed_regex $my_previous_directory | Where-Object {
                                                                                                            !$WhatIf -or
                                                                                                            ($WhatIf -and $_.FullName -notin $debug_my_prevfilespurged_fullnames )
                                                                                                        }

                    Remove-Old-Files $prev_files $keep_prev_count 1
                }

                $this.Status.postpostrotate = $true
            }
        }elseif ($compress) {
            # Compress

            if (!$dateext) {
                # Non-dateext: we'll just purge a single file. At least that's how it works the actual logrotate.

                if (!$delaycompress) {
                    # Compress previous file, Remove compression source file
                    # D:\console.log.1 -> D:\console.log.1.7z
                    Compress-File $my_previous_compressed_fullname $my_previous_fullname

                    # Remove expired file
                    Purge-File $my_expired_fullName
                }else {
                    # Remove expired file
                    Purge-File $my_expired_fullName
                }
            }else {
                $keep_prev_compressed_count = $rotate

                # Date extension
                if (!$delaycompress) {
                    # Compress previous file, Remove compression source file
                    # console.log.1 -> console.log.1.7z
                    Compress-File $my_previous_compressed_fullname $my_previous_fullname
                }else {
                    if ($rotate -gt 0) {
                        # One log is non-compressed because it's delayed.
                        $keep_prev_compressed_count = $rotate - 1
                    }
                }

                # Delete old compressed logs
                if ($rotate -gt 0) {
                    $prev_files = Get-Files $my_previous_compressed_regex $my_previous_directory | Where-Object {
                        !$WhatIf -or
                        ($WhatIf -and $_.FullName -notin $debug_my_prevfilespurged_fullnames )
                    }
                    Remove-Old-Files $prev_files $keep_prev_compressed_count 1
                }

            } # End if ($dateext)


            $this.Status.postpostrotate = $true

        } # End if ($compress)

        # Removed this - Script will spit stdout on the Pipeline
        #$this.Status.postpostrotate
    }

    $LogObject
}