classes/New-LogFactory.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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
}