include/module.utility.functions.ps1


function Get-ModuleConfiguration
{
    try
    {
        Get-Variable -Scope Global -Name ('ModuleConfiguration_{0}' -f ($MyInvocation.MyCommand.Module.Name)) -ValueOnly -ErrorAction Stop
    }
    catch
    {
        Write-Error -Message 'Failed to retrevie module configuration' -ErrorRecord $_
    }
}

function Initialize-ModuleConfiguration
{
    # Define ModuleConfiguration class
    class ModuleConfiguration
    {
        [string]$ModuleName = ''
        [string]$ModuleRootPath = ''
        [string]$ModuleManifestPath = ''
        [hashtable]$ModuleFolders = @{}
        [hashtable]$ModuleFiles = @{}
        [hashtable]$ModuleFilePaths = @{}
        $ModuleManifest

        [void] CollectModuleFilePaths ()
        {
            Get-ChildItem -Path $this.ModuleRootPath -Recurse -File | ForEach-Object {
                $this.ModuleFilePaths.($PSItem.Name) = $PSItem.FullName
            }
        }

        [void] ImportFiles ()
        {
            foreach ($File in (Get-ChildItem -Path $this.ModuleFolders.Settings -File -Recurse))
            {
                try
                {
                    $Content = $null
                    switch ($File.Extension)
                    {
                        '.csv' { $Content = Import-Csv $File.FullName -Delimiter ';' -Encoding UTF8 -ErrorAction Stop }
                        '.psd1' { $Content = Import-PowerShellDataFile -Path $File.fullname -ErrorAction Stop }
                        '.json' { $Content = Get-Content -Path $File.FullName -ErrorAction Stop -Raw | ConvertFrom-Json -ErrorAction Stop }
                        '.cred' { $Content = Import-Clixml -Path $File.FullName  -ErrorAction Stop }
                        default
                        {
                            Write-Warning -Message ('Failed to import configuration file {0}, unknown extension' -f $File.Name)
                            $Content = $null
                        }
                    }
                    if ($Content)
                    {
                        $null = $this.ModuleFiles.Add($File.BaseName, $Content)
                    }
                }
                catch
                {
                    Write-Error -Message ('Failed to import configuration {0} with error: {1}' -f $File.BaseName, $_.exception.message)
                }
            }
        }

        ModuleConfiguration (
            $MyInvoc
        )
        {
            # ModuleName
            $this.ModuleName = $MyInvoc.MyCommand.Module.Name

            # ModuleRootPath
            $this.ModuleRootPath = $MyInvoc.PSScriptRoot

            # ModuleFolders
            Get-ChildItem -Path $this.ModuleRootPath -Directory | ForEach-Object { $null = $this.ModuleFolders.Add($_.Name, $_.Fullname) }

            # ModuleManifestPath
            $this.ModuleManifestPath = (Join-Path -Path $this.ModuleRootPath -ChildPath ('{0}.psd1' -f $this.ModuleName))

            # ModuleManifest
            $this.ModuleManifest = Import-PowerShellDataFile -Path $this.ModuleManifestPath

            # ModuleFiles
            $this.ImportFiles()

            # ModuleFilePaths
            $this.CollectModuleFilePaths()
        }
    }

    # Store module configuration
    try
    {
        $null = New-Variable -Scope Global -Name ('ModuleConfiguration_{0}' -f ($MyInvocation.MyCommand.Module.Name)) -Value ([ModuleConfiguration]::New($MyInvocation)) -Force -ErrorAction Stop
    }
    catch
    {
        Write-Error -Message 'Failed to store ModuleConfiguration' -ErrorRecord $_
    }
}

function pslog
{
    [cmdletbinding()]
    param(
        [parameter(Position = 0)]
        [ValidateSet('Success', 'Info', 'Warning', 'Error', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string]
        $Severity,

        [parameter(Mandatory, Position = 1)]
        [string]
        $Message,

        [parameter(position = 2)]
        [string]
        $source = 'default',

        [parameter(Position = 3)]
        [switch]
        $Throw
    )

    begin
    {
        $localappdatapath = [Environment]::GetFolderPath('localapplicationdata') # ie C:\Users\<username>\AppData\Local
        $modulename = (Get-ModuleConfiguration).ModuleName
        $logdir = "$localappdatapath\$modulename\logs"
        $logdir | Assert-FolderExists
        $timestamp = (Get-Date)
        $logfilename = ('{0}.log' -f $timestamp.ToString('yyy-MM-dd'))
        $timestampstring = $timestamp.ToString('yyyy-MM-ddThh:mm:ss.ffffzzz')
    }

    process
    {
        switch ($Severity)
        {
            'Success'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                Write-Host -Object "SUCCESS: $timestampstring`t$source`t$message" -ForegroundColor Green
            }
            'Info'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                Write-Information -Message "$timestampstring`t$source`t$message"
            }
            'Warning'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                Write-Warning -Message "$timestampstring`t$source`t$message"
            }
            'Error'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                Write-Error -Message "$timestampstring`t$source`t$message"
                if ($throw)
                {
                    throw
                }
            }
            'Verbose'
            {
                if ($VerbosePreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                Write-Verbose -Message "$timestampstring`t$source`t$message"
            }
            'Debug'
            {
                if ($DebugPreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                Write-Debug -Message "$timestampstring`t$source`t$message"
            }
        }
    }
}

function Write-PSProgress
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,

        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,

        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')]
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )

    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Remove the global starttime variable if the Completed switch parameter is users
        try
        {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        }
        catch
        {
            throw $_
        }
    }
    elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime)
        {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        }
        else
        {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    }
    else
    {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }

    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName)
    {
        'Standard'
        {
            # Only update progress is any of the following is true
            # - DynamicUpdateFrequency is disabled
            # - Counter matches a mod of defined frequecy
            # - Counter is 0
            # - Counter is equal to Total (completed)
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total))
            {

                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)

                # Define count progress string status
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)

                # If percent would turn out to be more than 100 due to incorrect total assignment revert back to 100% to avoid that write-progress throws
                if ($Percent -gt 100) { $Percent = 100 }

                # Define write-progress splat hash
                $WriteProgressSplat = @{
                    Activity         = $Activity
                    PercentComplete  = $Percent
                    CurrentOperation = $Target
                }

                # Add ID if specified
                if ($Id) { $WriteProgressSplat.Id = $Id }

                # Add ParentID if specified
                if ($ParentId) { $WriteProgressSplat.ParentId = $ParentId }

                # Calculations for either timestats and without
                if ($NoTimeStats)
                {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                }
                else
                {
                    # Total seconds elapsed since start
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds

                    # Calculate items per sec processed (IpS)
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))

                    # Calculate seconds spent per processed item (for ETA)
                    $SecondsPerItem = if ($Counter -eq 0) { 0 } else { ($TotalSeconds / $Counter) }

                    # Calculate seconds remainging
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing

                    # Calculate ETA
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())

                    # Add findings to write-progress splat hash
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed'
        {
            Write-Progress -Activity $Activity -Id $Id -Completed
        }
    }
}

filter Assert-FolderExists
{
    $exists = Test-Path -Path $_ -PathType Container
    if (!$exists)
    {
        Write-Verbose "$_ did not exist. Folder created."
        $null = New-Item -Path $_ -ItemType Directory
    }
}

filter Invoke-GarbageCollect
{
    [system.gc]::Collect()
}