CheckBackups.psm1

using module EmailAutoresponder 
using module SimpleLogger

<#
 
.SYNOPSIS
    Checks if backups are not missing, their sizes is growing correctly and if there are any issues,
    then it is sending notification by email.
 
.PARAMETER PSScriptRoot
    Default working location for script, containing .ini file etc.
 
.PARAMETER ParameterSplitSymbol
    Symbol that is used as a splitter in .ini file to split arguments.
 
.PARAMETER IniFileLocation
    Location where is located ini file. Ini file contains input parameters for module.
 
.PARAMETER SkipCheckingAverageHotBackupSize
    If there are any cold backups, script calculates last two cold backups sizeavarage,
    and checks if hot backups are in range o it. Range is declared by function argument AvarageRange.
 
.PARAMETER AverageRange
    Percentage value that declares maximum range, that hot copy size can vary from avarage of two last cold backup size.
 
.OUTPUTS
    Returns 1 when .ini file not found.
 
#>

function Check-Backups {
    [CmdletBinding()]
    Param(
        [String]$PSScriptRoot = "C:\Work",
        [String]$ParameterSplitSymbol = "&",
        [String]$IniFileName = "CheckBackups.ini",
        [String]$IniFileLocation = (Join-Path $PSScriptRoot $IniFileName),
        [String]$LogFileName = "CheckBackups.log",
        [String]$LogFilePath = (Join-Path $PSScriptRoot $LogFileName),
        [String]$emailFrom = "powiadomienia.madler@gmail.com",
        [String]$emailTo = "patryk.milewski@gmail.com",
        [String]$emailPassword = "powiadomienia123",
        [String]$emailSubject = "Check-Backups Powershell script problem found.",
        [String]$emailSmtp = "smtp.gmail.com",
        [String]$emailPortsmtp = "587",
        [Switch]$SendEmailNotifications,         # by default false
        [Switch]$CheckSizeChange = $True,
        [Int]$AverageRange = 20
    )

    BEGIN {
        Write-Verbose "Starting Check-Backups function."
        Write-Verbose "Starting BEGIN block of Check-Backup function."    

        function Print-Parameters([String]$parameters) {
            Write-Verbose ("Printing parameters for line: " + $parameters)
            $counter = 1
            $line = ""
            $splittedParameters = $parameters.Split("{" + $ParameterSplitSymbol + "}")
            foreach ($parameter in $splittedParameters) {
                $line += " $counter" + ": $parameter"
                $counter++
            }
            Write-Verbose ("Printing parameters returning final line: " + $line)
            return $line
        }

        function Get-Size([String]$path) {
            ((Get-ChildItem -path $path -recurse | Measure-Object -property length -sum).sum / 1kB)
        }
        
    }

    PROCESS {
        Write-Verbose "Starting PROCESS block of Check-Backup function."    

        if (Test-Path $IniFileLocation) {
            Write-Verbose ("Reading input from ini file in path: $IniFileLocation")
            $iniFile = [System.IO.File]::OpenText($IniFileLocation)
            $iniFile = Get-Content $IniFileLocation

            $logger = New-Object SimpleLogger($LogFileName, $LogFilePath)
            [EmailAutoresponder]$emailAutoresponder = New-Object EmailAutoresponder($emailFrom, $emailTo, $emailPassword, $emailSubject, $emailSmtp, $emailPortsmtp)

            foreach ($line in $iniFile) {
                if ($line[0] -eq "#" -or $line.Length -eq 0) {
                    Write-Verbose ("Skipping empty or comment line: $line")
                    continue
                }
                
                Write-Verbose ("Executing line with parameters:" + (Print-Parameters $line))
                $splittedLine = $line.Split("{" + $ParameterSplitSymbol + "}")
                $backupPath = $splittedLine[0]

                $hotBackupWeekly = ($splittedLine[1] -like "true")
                $sendEmailNotification = ($splittedLine[2] -like "true")
                $expectedColdMinSizeChange = [Int]$splittedLine[3]
                $expectedColdMaxSizeChange = [Int]$splittedLine[4]                

                $emailAutoresponder.sendMail = $sendEmailNotification                

                [Backup]$backup = New-Object Backup($backupPath, $logger, $emailAutoresponder)
               
                if ($CheckSizeChange -eq $True) {
                    $backup.SetSizeChangesConsts($expectedColdMinSizeChange, $expectedColdMaxSizeChange, $AverageRange)
                }

                if ($hotBackupWeekly -eq $True) {
                    $backup.ValidateStandardBackups()
                }
                else {
                    $backup.ValidateNotStandardBackups()
                }
            }
        }
        else {
            Write-Error ("Ini file not found, cannot proceed program execution. Provided path: $IniFileLocation")
            return 1
        }

        if ($emailAutoresponder -and $SendEmailNotifications -eq $True) {
            Write-Verbose 'Trying to send email notification.'
            if ($emailAutoresponder.SendEmail() -eq $True) {
                $logger.AddLog("Successfuly sent email notification to: $emailTo")
            }
        }
    }

    END {}
    
}

Class Backup {
    [String]$backupPath
    [Int]$expectedMinColdSizeChange
    [Int]$expectedMaxColdSizeChange
    [int]$averageHotBackupChange
    [Switch]$validateBackupSize = $False
    [Switch]$noColdBackups = $True
    [String[]]$daysOfTheWeek = @("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
    [SimpleLogger]$logger
    [EmailAutoresponder]$emailAutoresponder
    
    Backup($backupPath, [ref]$logger, [ref]$emailAutoresponder) {
        $this.backupPath = $backupPath  
        $this.logger = $logger.Value
        $this.emailAutoresponder = $emailAutoresponder.Value
    }

    SetSizeChangesConsts($expectedMinColdSizeChange, $expectedMaxColdSizeChange, $averageRange) {
        $this.expectedMinColdSizeChange = $expectedMinColdSizeChange
        $this.expectedMaxColdSizeChange = $expectedMaxColdSizeChange
        $this.averageHotBackupChange = $averageRange
        $this.validateBackupSize = $True
    }

    ValidateStandardBackups() {
        Write-Verbose ("Listing directories in backup path: {0}" -f $this.backupPath)
        if (!(Test-Path $this.backupPath)) {
            $this.AddLogAndAppendToBody("Provided backup path doesn't exists: {0}" -f $this.backupPath)
            return
        }

        $foldersListInPath = Get-ChildItem -Directory -Name -Path $this.backupPath
        $coldBackups = @()
        Write-Verbose ("Looking for cold backups in backup path.")
        foreach ($folderName in $foldersListInPath) {
            if ($folderName -match "^[\d\.]+$" -and $folderName.Length -eq 8) {
                $coldBackups += @([Int]$folderName)
            }
        }
        Write-Verbose ("Cold backups found: $coldBackups")

        [Array]::Sort($coldBackups)
        $this.logger.AddLog(("Found {0} cold backups in path: {1}" -f $coldBackups.Count, $this.backupPath))

        if ($this.validateBackupSize) {
            $firstPath = $True
            $previousSize = 0
            foreach ($path in $coldBackups) {
                if ($firstPath -eq $True) {
                    $previousSize = Get-Size (Join-Path $this.backupPath $path)
                    $firstPath = $False
                    continue
                }
                $fullPath = Join-Path $this.backupPath $path
                $backupSize = Get-Size $fullPath
                $sizeDiff = $backupSize - $previousSize
                $previousSize = $backupSize
                $this.CheckColdBackupSize($backupSize, $sizeDiff, $fullPath)                            
            }
        }

        if ($coldBackups.Count -gt 0) {
            $this.noColdBackups = $False
        }
        else {
            $this.noColdBackups = $True
        }

        $averageOfLastTwoSize = 0
        if ($this.validateBackupSize -eq $True) {
            if ($coldBackups.Count -eq 1) {
                $averageOfLastTwoSize = Get-Size (Join-Path $this.backupPath $coldBackups[0])
            }
            elseif ($coldBackups.Count -gt 1) {
                $averageOfLastTwoSize += Get-Size (Join-Path $this.backupPath $coldBackups[$coldBackups.Count - 1])
                $averageOfLastTwoSize += Get-Size (Join-Path $this.backupPath $coldBackups[$coldBackups.Count - 2])
                $averageOfLastTwoSize /= 2
            }
        }

        foreach ($day in $this.daysOfTheWeek) {
            if (!(Test-Path (Join-Path $this.backupPath $day))) {
                $this.AddLogAndAppendToBody(("Missing {0} backup in a path: {1}" -f $day, $this.backupPath))
                continue
            }

            if ($this.validateBackupSize -eq $True) {
                $fullPath = Join-Path $this.backupPath $day
                $backupSize = Get-Size $fullPath
                $difference = [math]::abs($backupSize - $averageOfLastTwoSize)
                $this.CheckHotBackupSize($backupSize, $difference, $averageOfLastTwoSize, $fullPath)
            }
        }
    }

    ValidateNotStandardBackups() {
        $this.AddLogAndAppendToBody("Unimplemented method call - ValidateNotStandardBackups in class Backup. Check module ini file.")
    }

    hidden CheckColdBackupSize($backupSize, $sizeDiff, $fullPath) {
        if ($backupSize -eq 0) {
            $this.AddLogAndAppendToBody("Size of a $fullPath is 0. Probably missing backup.")
        }
        elseif ($sizeDiff -ge $this.expectedMinColdSizeChange -and $sizeDiff -le $this.expectedMaxColdSizeChange) {
            return
        }
        else {
            $this.AddLogAndAppendToBody(("Size difference of a backup {0} is not inside expected range, found: {1} expected: < {2} ; {3} >" -f $fullPath, $sizeDiff, $this.expectedMinColdSizeChange, $this.expectedMaxColdSizeChange))            
        }
    }

    hidden CheckHotBackupSize($backupSize, $sizeDiff, $averageOfLastTwoSize, $fullPath) {
        if ($backupSize -eq 0) {
            $this.AddLogAndAppendToBody("Backup in path $fullPath is empty.")
        }
        if ($this.noColdBackups -eq $False -and $sizeDiff -gt ($averageOfLastTwoSize * $this.averageHotBackupChange / 100)) {
            $this.AddLogAndAppendToBody(("Difference between copy in a path: {0} is too big. Expected: {1}% from {2} found: $sizeDiff" -f $fullPath, $this.averageHotBackupChange, $averageOfLastTwoSize))
        }
    }

    hidden AddLogAndAppendToBody($log) {
        $this.logger.AddLog($log)
        $this.emailAutoresponder.AppendToBody($log + "`n")
    }
}

Export-ModuleMember -Function Check-Backups