classes/New-LogFactory.ps1
function New-LogFactory { ####################### # LogFactory Class # ####################### # LogFactory is a stateful factory that constructs Log Objects, and tracks their last rotation status. $LogFactory = [PSCustomObject]@{ 'LogObjects' = New-Object System.Collections.ArrayList 'Status' = @{} 'StatusFile_FullName' = if ( $MyInvocation.PSCommandPath ) { # Use the calling script's directory if so "$( Split-Path $MyInvocation.PSCommandPath -parent )$( [IO.Path]::DirectorySeparatorChar )Log-Rotate.status" }else { # Or fallback on the current working directory Join-Path $(Get-Location) 'Log-Rotate.status' } } $LogFactory | Add-Member -Name 'InitStatus' -MemberType ScriptMethod -Value { param ([string]$statusfile_path) # If no status file is specified, we'll consider it to be in script directory called 'Log-Rotate.status' if (!$statusfile_path) { $statusfile_path = $this.StatusFile_FullName } if ($statusfile_path) { # Ensure status file path contains valid characters try { $exists = Test-Path -LiteralPath $statusfile_path -ErrorAction Stop }catch { # Illegal characters in path Write-Error "STATUSFILE: WARNING: Invalid status file $statusfile_path" -Continue throw } if ($exists) { # Ensure it's not an existing diretory $item = Get-Item $statusfile_path -ErrorAction Stop if (Test-Path $item.FullName -PathType Container) { throw "STATUSFILE: WARNING: Invalid status file $statusfile_path . It points to an existing directory $($item.FullName)." } try { # Make it an absolute path, if it is not $this.StatusFile_FullName = Convert-Path $statusfile_path Write-Verbose "status file: $( $this.StatusFile_FullName )" # Read status $status = Get-Content $this.StatusFile_FullName -Raw }catch { Write-Error "STATUSFILE: WARNING: Status file $( $this.StatusFile_FullName ) could not be read." -ErrorAction Continue throw } }else { # Create a new status file, creating all directories if needed. If a relative path was given, it will be resolved to the current working directory. try { #[io.file]::OpenWrite($statusfile_path).close() $item = New-Item -Path $statusfile_path -ItemType File -Force -ErrorAction Stop if ($item) { # Store state file fullname (absolute path). $this.StatusFile_FullName = $item.FullName $this.DumpStatus() Write-Verbose "new status file created: $( $this.StatusFile_FullName )" }else { throw } ## NOTE: Not using this, because debugging should also test the creation of a file. # The reason for using the following code is only because the cmdlets such as Convert-Path, Resolve-Path must point to an existing item. # If debugging didn't create the file, we would have to manually normalize the status file path (i.e. get it's absolute path). <# if ($WhatIf) { $is_home = $statusfile_path -match '^~' if ($is_home) { # It's an absolute path $parent = Convert-Path '~' $child = $statusfile_path -replace '^~', '' $this.StatusFile_FullName = Join-Path -Path $parent -ChildPath $child }else { if ( ! [System.IO.Path]::IsPathRooted($statusfile_path) ) { # A relative path was provided. # Can't use Convert-Path / Resolve-Path which must point to an existing item # Build the absolute path to the status file. # E.g. 'D:\mycwd\Log-Rotate.status' -> 'D:\mycwd\Log-Rotate.status' # E.g. 'D:\mycwd\.\Log-Rotate.status' -> 'D:\mycwd\Log-Rotate.status' # E.g. 'D:\mycwd\..\Log-Rotate.status' -> 'D:\Log-Rotate.status' # E.g. 'D:\mycwd\..\test\Log-Rotate.status' -> 'D:\test\Log-Rotate.status' $path = Join-Path -Path $PWD.Path -ChildPath $statusfile_path $this.StatusFile_FullName = [System.IO.Path]::GetFullPath( $path ) }else { # An absolute path was provided. Standardize the slashes to platform-specific slashes ([IO.Path]::DirectorySeparatorChar) $this.StatusFile_FullName = [System.IO.Path]::GetFullPath( $statusfile_path ) } } Write-Verbose "new status file created: $( $this.StatusFile_FullName )" } #> }catch { Write-Error "STATUSFILE: WARNING: Status file $statusfile_path could not be created" -ErrorAction Continue throw } } } # Parse and store previous rotation status if ($status) { $lines = $status.split("`n") # The first line must be a Log-Rotate state file title, if not we might be dealing with another file. if ( $lines[0] -notmatch 'Log\-Rotate state' ) { throw "Log-Rotate state file $( $this.StatusFile_FullName ) is of the wrong format. Check that you are not overriding another file. If you are not, delete the file and try again." } $lines.Trim() | Where-Object { $_ } | ForEach-Object { $matches = [Regex]::Matches($_, '"([^"]+)" (.+)') if ($matches.success) { $path = $matches.Groups[1].Value $lastRotateDate = $matches.Groups[2].Value if (Test-Path $path -PathType Leaf) { try { $lastRotateDatetime = Get-Date -Date $lastRotateDate -Format 's' -ErrorAction SilentlyContinue $this.Status[$path] = $lastRotateDatetime }catch {} } } } } # Always test for write permissions on the status file try { '' | Out-File $this.StatusFile_FullName -Append -Force if (!$status -and $WhatIf) { # We're running Log-Rotate the first time in debug mode. Remove-Item $this.StatusFile_FullName } }catch { Write-Error "STATUSFILE: WARNING: Insufficient write permissions for status file $( $this.StatusFile_FullName ). Resolve this error before continuing." -ErrorAction Continue throw } } $LogFactory | Add-Member -Name 'Create' -MemberType ScriptMethod -Value { param ([System.IO.FileInfo]$logfile, [hashtable]$options) function Get-Status([System.IO.FileInfo]$file) { $lastRotationDate = if ($this.Status.ContainsKey($file.FullName)) { $this.Status[$file.FullName] }else { '' } [string]$lastRotationDate } $lastRotationDate = Get-Status $logfile $_logObject = $LogObject.New($logfile, $options, $lastRotationDate) if ($_logObject) { $this.LogObjects.Add($_logObject) | Out-Null return $_logObject } $null } $LogFactory | Add-Member -Name 'GetAll' -MemberType ScriptMethod -Value { return $this.LogObjects } $LogFactory | Add-Member -Name 'DumpStatus' -MemberType ScriptMethod -Value { try { if (!$WhatIf) { # Update my state with each logs rotation status $this.GetAll() | Where-Object { $_.Status['rotation_datetime'] } | ForEach-Object { $rotationDateISO = $_.Status['rotation_datetime'].ToString('s') $lastRotationDateISO = if ($this.Status.ContainsKey($_.Logfile.FullName)) { $this.Status[$_.Logfile.FullName] } else { '' } if ( !$lastRotationDateISO -or ($rotationDateISO -gt $lastRotationDateISO) ) { Write-Verbose "Updating status of rotation for log $($_.Logfile.FullName) " $this.Status[$_.Logfile.FullName] = $rotationDateISO }else { Write-Verbose "Not updating status of rotation for log $($_.Logfile.FullName) " } } # Dump state file Write-Verbose "Writing status file to $($this.StatusFile_FullName)" $output = "Log-Rotate state - version $LogRotateVersion" $this.Status.Keys | ForEach-Object { $output += "`n`"$_`" $($this.Status[$_])" } $output | Out-File $this.StatusFile_FullName -Encoding utf8 }else { # Dump state file Write-Verbose "Writing status file to $($this.StatusFile_FullName)" } }catch { Write-Error "Failed to write state file." -ErrorAction Continue throw } } $LogFactory } |