ConvertTo-HTML Challenge

Thank you for taking the time to read through this post and help me out. I have been reading Learn PowerShell 3 In a Month of Lunches (3 times) and finishing the last couple of chapters of Learn PowerShell scripting in a month of Lunches. I’m looking forward to Learn PowerShell Toolmaking in a Month of Lunches. These books have really helped me get a foundation in PowerShell and what I can do with that, including how I think about using PowerShell.

With that said, a recent issue at work has given me an opportunity to built a set of tools and a controller script to monitor some backup files. As it is only my second set of tools I have developed, I admittedly am still refining some rough edges, but I have it working pretty smooth now except for one function, and that is the conversion of the object to HTML and then sending it to the last tool for email delivery.

This is the controller script:

$Backup = Get-CheckBackup -Path "\\Server\d$\Folder\"

$day = Get-Date

$Failed = $Backup | Where-Object {$_.Status -eq "Failed"}

$Successful = $Backup | Where-Object {$_.Status -eq "Successful"}

If ($Failed -ne $Null) {
    $Failed | ConvertTo-Html | Out-File -FilePath "\\Server\D$\Folder\Logs\FailedBackup.txt"
    $FailedBack = Get-Content -Path "\\Server\D$\Folder\Logs\FailedBackup.txt"
    Send-BackupFileFail -To "NetworkMonitoring@Company.org" -Files $FailedBack
} #If

IF ($Successful -ne $Null) {
    $day | Select-Object -Property Date | ConvertTo-Html | Out-File -FilePath "\\Server\D$\Folder\Logs\SuccessfulBackup.txt" -Append

    $Successful | ConvertTo-HTML | Out-File -FilePath "\\Server\D$\Folder\Logs\SuccessfulBackup.txt" -Append

    If ($day.DayOfWeek -eq "Sunday") {
        $success = Get-Content -Path "\\Server\d$\Folder\logs\SuccessfulBackup.txt"

        Send-IHCRCBackupFileSuccess -To "NetworkMonitoring@Company.org" -Files $success

        Remove-Item -Path "\\Server\d$\Folder\logs\SuccessfulBackup.txt"
    } #If
} #IF 

The script starts by calling my first tool, which collects the files in the patch, checks their LastWriteDate to a date specified in the function call, in this case, the default of 0 or today, and determines if the backup was successful or failed, and then creates and object which is then placed in an array. This is all saved into a variable in the controller script.

Get-CheckBackup function

Function Get-CheckBackup {
     Get-CheckBackup

    Status     FullName                    LastWriteTIme
    ------     --------                    -------------
    Failed     C:\Demo\HelloWorld.ps1      2/12/2018 2:30:38 PM
    Successful C:\Demo\NewFile.txt         6/21/2018 10:05:28 AM
    Failed     C:\Demo\TestFailedFile1.txt 6/20/2018 3:50:36 PM

    In this example, the function is run using all the defaults, dasy of 0, current path and no recurse.  Files with a lastWriteTime that does not match today's date have a status of "Failed."

    .EXAMPLE

    PS C:\> Get-CheckBackup -days 0 -Paths "c:\Demo\NewFile.txt"

    Status     FullName            LastWriteTIme
    ------     --------            -------------
    Successful C:\Demo\NewFile.txt 6/21/2018 10:05:28 AM

    In this example, the days were set to zero, and a path to a specific file is set.  The function returns the status of the file based on the LastWriteTime
    
    .EXAMPLE

    PS C:\> Get-CheckBackup -days 1 -Paths "c:\Demo" -Recurse

    Status     FullName                                         LastWriteTIme
    ------     --------                                         -------------
    Failed     C:\Demo\HelloWorld.ps1                           2/12/2018 2:30:38 PM
    Successful C:\Demo\NewFile.txt                              6/21/2018 10:05:28 AM
    Successful C:\Demo\TestFailedFile1.txt                      6/20/2018 3:50:36 PM
    Failed     C:\Demo\Demo Plus Folder\Eventlogapplication.htm 3/9/2018 11:40:57 AM
    Successful C:\Demo\Demo Plus Folder\New File Also.txt       6/21/2018 10:06:10 AM

    In this example, the days to check against was 1, and a path of "C:\Demo" was set.  This displays all the files in the current and all sub folders, and sets a status of "successful" on files that have a lastwritetime of 1 day or less.

    .EXAMPLE

    PS C:\> $Days = 2
    PS C:\> $Backups = "C:\test","C:\Demo"
    PS C:\> Get-CheckBackup -Days $Days -Paths $Backups -Recurse

    Status     FullName                                         LastWriteTIme
    ------     --------                                         -------------
    Successful C:\test\Demo Test File.txt                       6/21/2018 11:08:12 AM
    Failed     C:\test\New Hire Procedure.docx                  6/6/2017 1:01:15 PM
    Failed     C:\test\Termination Procedure.docx               6/7/2017 3:44:52 PM
    Failed     C:\test\Workstation Retire Procedure.docx        5/24/2017 9:59:57 AM
    Failed     C:\test\PowerShellTest\30days.csv                4/24/2018 4:20:36 PM
    Failed     C:\test\PowerShellTest\Get-FolderSize.ps1        11/8/2017 11:09:46 AM
    Successful C:\test\PowerShellTest\I Updated Today.txt       6/21/2018 11:08:28 AM
    Failed     C:\test\PowerShellTest\Install-WMF5.1.ps1        3/22/2017 1:44:48 PM
    Failed     C:\Demo\HelloWorld.ps1                           2/12/2018 2:30:38 PM
    Successful C:\Demo\NewFile.txt                              6/21/2018 10:05:28 AM
    Successful C:\Demo\TestFailedFile1.txt                      6/20/2018 3:50:36 PM
    Failed     C:\Demo\Demo Plus Folder\Eventlogapplication.htm 3/9/2018 11:40:57 AM
    Successful C:\Demo\Demo Plus Folder\New File Also.txt       6/21/2018 10:06:10 AM

    In this example, The days and the path is set by the variables $Days and $Backups.  This lists all the files in the folders and sub folders, marking fiels with a lastWritetime of 2 days or less as "successful," and sets the remaining files status as failed.
    #>
    [CmdletBinding(SupportsShouldProcess)]

    param (

        [String] $Days = 0,

        [parameter(ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true)]
        [String[]] $Paths = (Get-Location),

        [switch] $recurse
    )

    BEGIN {
        Write-Verbose "Initializing some variables that are used through function"
        $day = (Get-Date).AddDays(-$days).ToString('MM/dd/yyyy')
        $Log = @()
    } #Begin

    PROCESS {
        ForEach ($path in $paths) {
            Write-Verbose "Collecting the files to verify the backup status"
            If ($recurse -eq $True) {
                $Files = Get-ChildItem -Path $path -File -Recurse
            } #If
            else {
                $files = Get-ChildItem -Path $path -File
            } #Else

            ForEach ($File in $Files) {
                Wite-Verbose "Collecting the files that failed their backup"
                $succeed = $file.LastWriteTime -lt $day
                Write-Verbose "Collecting properties about the failed backup files"
                If ($succeed -eq $true) {
                    $props = @{'FullName'=$File.FullName
                               'LastWriteTIme'=$file.LastWriteTime
                               'Status'='Failed'}
                    $obj = New-Object -TypeName PSObject -Property $props
                } #If
                Write-Verbose "Collecting files which had a successful backup"
                elseif ($succeed -eq $false) {
                    Write-Verbose "Collecting properties about the successful backup files"
                    $props = @{'FullName'=$File.FullName
                               'LastWriteTIme'=$file.LastWriteTime
                               'Status'='Successful'}
                    $obj = New-Object -TypeName PSObject -Property $props
                } #Elseif 
                Write-Verbose "Adding each record to an array"
                $log += $obj
            } #ForEach Comapre
        } #ForEach Get Files
    } #Process

    END {
        Write-Verbose "Dispalying the results of data collection"
        Write-Output $log
    } #End
} #Function 

The controller script then separates the objects into the variable $Failed for files that did not backup up properly, and $Successful for successful backups. This is where I run into the problem. If I pipe the variable to ConvertTo-HTML | Send-Email tool I built, or even just try and pipe the object, it works, but it creates a separate email for each and every object, even blank lines.

The only solution I have found is to convert the objects in the variable using ConvertTo-HTML then pipe it to OutFile and save the file. After that is done, I have to use Get-Content to populate another variable, and I can then use that Variable to populate the backup file path in the email body to show what file/s either missed their backup or were successful.

Send-BackupFileFail function

 Function Send-BackupFileFail {


[CmdletBinding(SupportsShouldProcess)]

param (

    [parameter(Mandatory=$true, 
               HelpMessage = "Enter the email address or addresses to send message to",
               ValueFromPipelineByPropertyName=$True)]
    [validatenotnullorempty()]
    [Alias('Send')]
    [String[]] $To,

    [parameter(Mandatory=$True,
               ValueFromPipeline=$True,
               ValueFromPipelineByPropertyName=$True)]
    [validatenotnullorempty()]
    [String[]] $Files

)

BEGIN {}


PROCESS {
    Write-Verbose "Sending the email notfication with backup files that failed"
    Send-MailMessage -To $To `
        -Subject "Failed Bakup Files - $(Get-Date -DisplayHint Date)" `
        -SmtpServer "Email.Company.org" `
        -from "Monitoring@Company.org" `
        -BodyAsHtml `
        -Body "The following files have not completed a successful backukp since the corresponding date and time.
          
        $Files
        
        Perform any necessary troubleshooting to bring the backups up to date.
        
        The Company IT Team
        Company
        Ext:  1234
        Email:  ALL_IT_STAFF@Company.org
        www.Company.org"
    } #Process
} #Function 

Send-BackupFileSuccess function

 Function Send-BackupFileSuccess {


[CmdletBinding(SupportsShouldProcess)]

param (

    [parameter(Mandatory=$true, 
               HelpMessage = "Enter the email address or addresses to send message to",
               ValueFromPipelineByPropertyName=$True)]
    [validatenotnullorempty()]
    [Alias('Send')]
    [String[]] $To,

    [parameter(Mandatory=$True,
               ValueFromPipeline=$True,
               ValueFromPipelineByPropertyName=$True)]
    [validatenotnullorempty()]
    [String[]] $Files

)

BEGIN {}


PROCESS {
    Write-Verbose "Sending the email notification of successful backup files"
    Send-MailMessage -To $To `
        -Subject "Successful Backup Files Report" `
        -SmtpServer "email.company.org" `
        -from "Monitoring@company.org" `
        -BodyAsHtml `
        -Body "The following files have Successfully completed the backukp process for the the dates and times listed.
          
        $Files
        
        The Company IT Team
        Companyr
        Ext:  1234
        Email:  ALL_IT_STAFF@Company.org
        www.Company.org"
    } #Process
} #Function 

Is there something a missed? Is there a better way to insert these objects into the email body?

Thank you in advance for taking the time to look over the code and helping me to refine this process.

Daniel Olson

The Toolmaking book is no longer published; “Scripting” replaced it. See donjones.com/powershell for the book sequence these days.

When you’re piping objects to a function, the function only gets one item at a time. What you’d normally do with the receiving function in your case is to use a BEGIN block to set up some variables to accumulate the eventual email, build the email in the PROCESS block, and send it in the END block. PROCESS is what gets one object at a time in whatever variable is accepting the pipeline input.

E.g,

BEGIN { $body = "" }
PROCESS { $body += "whatever" }
END { # send it }

Your first function is, I think, incorrectly accumulating output in an array rather than outputting each one to the pipeline as it goes. See https://devops-collective-inc.gitbooks.io/the-big-book-of-powershell-gotchas/content/manuscript/accumulating-output-in-a-function.html. Unless you need to accumulate a batch of stuff and DO SOMETHING WITH IT before outputting, the pattern is to output-as-you-go and let the pipeline parallelize itself.

Don,

Thank for your valuable time and input. That information will help me a great deal. I kind of jumped in the “deep end” and each time I refine and improve the script, I learn a little more.

A quick side note, thank you and Jeff Hicks for publishing your Month of Lunches books, and the videos you have published. They have been a wonderful teaching tool and have given me the confidence to learn PowerShell.

Oh, you’re very welcome - thank you for buying them ;). Jeff’s got a great one - https://leanpub.com/psprimer - you might want to check out. Kind of a self-test way to figure out “what do I learn next?”

Check out some updates to your function to make you aware of some other ways to do things:

Function Get-CheckBackup {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [String] $Days = 0,
        [parameter(ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true)]
        [String[]] $Path = (Get-Location), #Use singular, try to use other cmdlets like Get-ChildItem as reference
        [switch] $Recurse
    )

    BEGIN {
        Write-Verbose "Initializing some variables that are used through function"
        $day = (Get-Date).AddDays(-$days).ToString('MM/dd/yyyy') #not being used
    } #Begin

    PROCESS {
        $results = ForEach ($p in $path) { #Collect the results 
            Write-Verbose "Collecting the files to verify the backup status"
            #check out about_Splatting
            $fileParams = @{
                Path = $p
                File = $true
            }
            #splatting allows you to dyamically build params. They are stored in hash table
            If ($recurse -eq $True) {$fileParams.Add("Recurse", $true)}
            
            $files = Get-ChildItem @fileParams

            ForEach ($File in $Files) {
 
                If ($file.LastWriteTime -lt $day) { #This is going to return True\False
                    Write-Verbose "Collecting properties about the failed backup files"                  
                    New-Object -TypeName PSObject -Property @{
                        'FullName'     =$File.FullName
                        'LastWriteTIme'=$file.LastWriteTime
                        'Status'       ='Failed'
                    }
                } #If
                else {
                    Write-Verbose "Collecting properties about the successful backup files"
                    New-Object -TypeName PSObject -Property @{
                        'FullName'     =$File.FullName
                        'LastWriteTIme'=$file.LastWriteTime
                        'Status'       ='Successful'
                    }
                } #Elseif 
            } #ForEach Comapre
        } #ForEach Get Files
    } #Process

    END {
        Write-Verbose "Displaying the results of data collection"
        $results
    } #End
} #Function 

The best way I’ve found to create an object from loops is to set a variable ($results = foreach…) and avoid += to do append operations. Anything that is outputted is automatically appended to $results. Splatting allows you build params dynamically and I think keeps the params looking neat in your code.

Don -

Thank you for suggesting The PowerShell Practice Primer by Jeff Hicks. That book is something that will help me tremendously. I also noticed The PowerShell Scripting and Toolmaking Book at the bottom. I look forward to getting both of those books in the very near future.

Rob Simmers -

Thank you for your input. It is very helpful and educational. Your suggestion on the Path variable name was very helpful. When I was writing it, I was trying to figure out how to make it singular on the parameter, yet have the ForEach read (Single in multiple) and just couldn’t figure out the best way to do it to make sense. I will look into splatting in more detail. I just started to play with them last week when I read about them in Powershell Scripting in a month of lunches, and this will help me get a good grasp on them.