CheckBackups.psm1

<#
 
.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.
 
#>

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 true
    )

    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 ine: " + $line)
            return $line
        }

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

    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 Logger($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")
                $expectedColdSizeChange = [Int]$splittedLine[3]

                $emailAutoresponder.sendMail = $sendEmailNotification                

                [Backup]$backup = New-Object Backup($backupPath, $expectedColdSizeChange, $logger, $emailAutoresponder)
                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
        }
    }

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

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

Class Logger {
    [String]$logFileName
    [String]$logFileLocation

    Logger([String]$logFIleName, [String]$logFileLocation) {
        Write-Verbose ("Constructing Logger Class instance with parameters 1: " + $logFileName + " 2: " + $logFileLocation)
        $this.logFileName = $logFileName
        $this.logFileLocation = $logFileLocation
        $this.Initialize()
        $this.AddLog("Log file created.")
    }

    Initialize() {
        Write-Verbose "Initializing Logger instance"
        if (!(Test-Path $this.logFileLocation)) {
            Write-Verbose "Log file not found, creating a new one"
            New-item -ItemType File $this.logFileLocation -Force
        }
    }

    AddLog([String]$newLog) {
        Write-Verbose ("Adding a new log: " + $newLog)
        "[" + (Get-Date) + ":] " + $newLog | Add-Content $this.logFileLocation
    }
}

Class Backup {
    [String]$backupPath
    [Int]$expectedColdSizeChange    
    [String[]]$daysOfTheWeek = @("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
    [Logger]$logger
    [EmailAutoresponder]$emailAutoresponder
    
    Backup($backupPath, $expectedColdSizeChange, [ref]$logger, [ref]$emailAutoresponder) {
        $this.backupPath = $backupPath
        $this.expectedColdSizeChange = $expectedColdSizeChange
        $this.logger = $logger.Value
        $this.emailAutoresponder = $emailAutoresponder.Value
    }

    ValidateStandardBackups() {
        Write-Verbose ("Listing directories in backup path: " + $this.backupPath)
        if (!(Test-Path $this.backupPath)) {
            $this.AddLogAndAppendToBody("Provided backup path doesn't exists: " + $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)
        
        $coldBackups = [Array]::Sort($coldBackups)
        $this.logger.AddLog("Found " + $coldBackups.Length + " cold backups in path: " + $this.backupPath)

        $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

            CheckBackupSize $backupSize $sizeDiff $this.expectedColdSizeChange $fullPath
        }

        foreach ($day in $this.daysOfTheWeek) {
            if (!(Test-Path (Join-Path $this.backupPath $day))) {
                $this.AddLogAndAppendToBody("Missing " + $day + " backup in a path: " + $this.backupPath)
                continue
            }
            $backupSize = Get-Size (Join-Path $this.backupPath $day)
            if ($backupSize -eq 0) {
                $this.AddLogAndAppendToBody("Backup in path " + (Join-Path $this.backupPath $day) + " is empty.")
            }
        }
    }

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

    hidden CheckBackupSize($backupSize, $sizeDiff, $expectedChange, $fullPath) {
        if (($expectedChange -gt 0 -and $sizeDiff -lt $expectedChange) -or ($expectedChange -lt 0 -and $sizeDiff -gt $expectedChange)) {
            $this.AddLogAndAppendToBody("Size difference of a " + $fullPath + " is not inside expected range: " + $expectedChange)
        }
        if ($backupSize -eq 0) {
            $this.AddLogAndAppendToBody("Size of a " + $fullPath + " is 0. Probably missing backup.")
        }
    }

    hidden AddLogAndAppendToBody($log) {
        $this.logger.AddLog($log)
        $this.emailAutoresponder.AppendToBody($log)
    }
}

Class EmailAutoresponder {
    [String]$from
    [String]$to
    [pscredential]$credential
    [String]$subject
    [String]$smtp
    [String]$port
    [String]$body = ""
    [Switch]$sendMail = $False
    [Switch]$isEmpty = $True
    
    EmailAutoresponder([String]$from, [String]$to, [String]$password, [String]$subject, [String]$smtp, [String]$port) {
        Write-Verbose ("Constructing EmailAutoresponder instance with parameters 1: " + $from + " 2: " + $to + " 3: " + $password + " 4: " + $subject + " 5: " + $smtp + " 6: " + $port)
        $this.from = $from
        $this.to = $to
        $secureString = ConvertTo-SecureString $password -AsPlainText -Force
        $this.credential = New-Object System.Management.Automation.PSCredential ($this.from, $secureString)
        $this.subject = $subject
        $this.smtp = $smtp
        $this.port = $port
    }

    [bool]SendMail() {
        if ($this.isEmpty -eq $True) {
            Write-Verbose "Email autoresponder body is empty, not sending an email."
            return $False
        }

        try {
            Write-Verbose ("Sending email notification with body: " + $this.body)
            Send-MailMessage -From $this.from -To $this.to -Credential $this.credential -Subject $this.subject -Body $this.body -SmtpServer $this.smtp -Port $this.port -UseSsl -Encoding UTF8 -ErrorAction Stop
            Write-Verbose "Successfully sent notification"
            return $True
        }
        catch {
            Write-Verbose "Failed to send notification"
            return $False
        }
    }

    AppendToBody($body) {
        if ($this.sendMail -eq $True) {
            Write-Verbose ("Appending to email body new text: " + $body)
            $this.body += $body
            $this.isEmpty = $False
        }
        else {
            Write-Verbose "Not appending to email body a new text, because of sendMail switch set to false."
        }
    }

    ResetResponder() {
        Write-Verbose "Reseting email autoresponder to default stance."
        $this.body = ""
        $this.isEmpty = $True
    } 
}

Export-ModuleMember -Function Check-Backups