DSCResources/Grani_S3Content/Grani_S3Content.psm1

#region Initialize

function Initialize {
    # Enum for Item Type
    Add-Type -TypeDefinition @"
        public enum GraniDonwloadItemTypeEx
        {
            FileInfo,
            DirectoryInfo,
            Other,
            NotExists
        }
"@
 -ErrorAction SilentlyContinue

    # Enum for Ensure
    Add-Type -TypeDefinition @"
        public enum GraniDonwloadEnsuretype
        {
            Present,
            Absent
        }
"@
 -ErrorAction SilentlyContinue

    # Enum for CheckSum
    Add-Type -TypeDefinition @"
        public enum GraniDonwloadCheckSumtype
        {
            FileHash,
            FileName
        }
"@
 -ErrorAction SilentlyContinue
}

Initialize

#endregion

#region Message Definition

$debugMessage = DATA {
    ConvertFrom-StringData -StringData "
        ExecuteScriptBlock = Execute ScriptBlock without Credential. '{0}'
        ExecuteScriptBlockWithCredential = Execute ScriptBlock with Credential. '{0}'
        FileExists = File found from DestinationPath.
        IsCheckSumFileName = CheckSum was '{0}', File already exist in destination path. Complete file checking.
        IsDestinationPathExist = Checking Destination Path is existing and Valid as a FileInfo
        IsDestinationPathAlreadyUpToDate = CheckSum was '{0}', matching FileHash to verify file is already Up-To-Date.
        IsFileAlreadyUpToDate = CurrentFileHash : S3 FileHash -> {0} : {1}
        IsS3ObjectExist = Testing S3 Object is exist or not.
        ItemTypeWasFile = Destination Path found as File : '{0}'
        ItemTypeWasDirectory = Destination Path found but was Directory : '{0}'
        ItemTypeWasOther = Destination Path found but was neither File nor Directory: '{0}'
        ItemTypeWasNotExists = Destination Path not found : '{0}'
        OverrideRegion = Overriding Region : '{0}'
        ValidateS3Bucket = Checking S3 Bucket '{0}' is exist.
        ValidateS3Object = Checking S3 Object Key '{0}' is exist.
        ValidateFilePath = Check DestinationPath '{0}' is FileInfo and Parent Directory already exist.
    "

}

$verboseMessage = DATA {
    ConvertFrom-StringData -StringData "
        AlreadyUpToDate = Current DestinationPath FileHash and S3 FileHash matched. File already Up-To-Date.
        NotUpToDate = Current DestinationPath FileHash and S3 FileHash not matched. Need to download latest file.
        ResultS3Bucket = S3Bucket exist status : S3Bucket {0}, Status : {1}
        ResultS3Object = S3Object exist status : S3Object {0}, Status : {1}
        StartS3Download = Downloading S3 Object.
    "

}
$exceptionMessage = DATA {
    ConvertFrom-StringData -StringData "
        DestinationPathAlreadyExistAsNotFile = Destination Path '{0}' already exist but not a file. Found itemType is {1}. Windows not allowed exist same name item.
        S3BucketNotExistEXception = Desired S3 Bucket not found exception. S3Bucket : {0}
        S3ObjectNotExistEXception = Desired S3 Object not found in S3Bucket exception. S3Bucket : {0}, S3Object : {1}
        ScriptBlockException = Error thrown on ScriptBlock. ScriptBlock : {0}
    "

}

#endregion

#region *-TargetResource

function Get-TargetResource {
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [System.String]$S3BucketName,

        [parameter(Mandatory = $false)]
        [System.String]$Key,

        [parameter(Mandatory = $true)]
        [System.String]$DestinationPath,

        [parameter(Mandatory = $false)]
        [System.String]$PreAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.String]$PostAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [parameter(Mandatory = $false)]
        [ValidateSet("FileHash", "FileName")]
        [System.String]$CheckSum = [GraniDonwloadCheckSumtype]::FileHash.ToString(),

        [parameter(Mandatory = $false)]
        [System.String]$Region = [string]::Empty
    )

    # Initialize return values
    # Header and OAuth2Token will never return as TypeConversion problem
    $returnHash = @{
        S3BucketName    = $S3BucketName
        Key             = $Key
        DestinationPath = $DestinationPath
        Ensure          = [GraniDonwloadEnsuretype]::Absent.ToString()
        PreAction       = $PreAction
        PostAction      = $PostAction
        Credential      = New-CimInstance -ClassName MSFT_Credential -Property @{Username = [string]$Credential.UserName; Password = [string]$null} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly
        CheckSum        = $CheckSum
        Region          = $Region
    }

    try {
        # Fail fast S3Bucket and S3Object existance.
        $isBucketExist = TestS3Bucket -BucketName $S3BucketName -Region $Region
        if (-not $isBucketExist) {        
            Write-Verbose -Message ($verboseMessage.ResultS3Bucket -f $S3BucketName, $isBucketExist);
            return $returnHash
        }
        $isObjectExist = TestS3Object -BucketName $S3BucketName -Key $Key -Region $Region;
        if (-not $isObjectExist) {        
            Write-Verbose -Message ($verboseMessage.ResultS3Object -f $Key, $isObjectExist);
            return $returnHash
        }

        # Start checking destination Path check if S3Bucket and S3Object exists
        Write-Debug -Message $debugMessage.IsDestinationPathExist
        $itemType = GetPathItemType -Path $DestinationPath

        $fileExists = $false
        switch ($itemType.ToString()) {
            ([GraniDonwloadItemTypeEx]::FileInfo.ToString()) {
                Write-Debug -Message ($debugMessage.ItemTypeWasFile -f $DestinationPath)
                $fileExists = $true
            }
            ([GraniDonwloadItemTypeEx]::DirectoryInfo.ToString()) {
                Write-Debug -Message ($debugMessage.ItemTypeWasDirectory -f $DestinationPath)
            }
            ([GraniDonwloadItemTypeEx]::Other.ToString()) {
                Write-Debug -Message ($debugMessage.ItemTypeWasOther -f $DestinationPath)
                return $returnHash
            }
            ([GraniDonwloadItemTypeEx]::NotExists.ToString()) {
                Write-Debug -Message ($debugMessage.ItemTypeWasNotExists -f $DestinationPath)
                return $returnHash
            }
        }

        # Already Up-to-date Check
        if ($fileExists -eq $true) {
            Write-Debug -Message $debugMessage.FileExists
            switch ($CheckSum) {
                ([GraniDonwloadCheckSumtype]::FileHash.ToString()) {
                    Write-Debug -Message ($debugMessage.IsDestinationPathAlreadyUpToDate -f $CheckSum)
                    $currentFileHash = GetFileHash -Path $DestinationPath
                    $s3ObjectCache = GetS3ObjectHash -BucketName $S3BucketName -Key $Key -Region $Region

                    Write-Debug -Message ($debugMessage.IsFileAlreadyUpToDate -f $currentFileHash, $s3ObjectCache)
                    if ($currentFileHash -eq $s3ObjectCache) {
                        Write-Verbose -Message $verboseMessage.AlreadyUpToDate
                        $returnHash.Ensure = [GraniDonwloadEnsuretype]::Present.ToString()
                    }
                    else {
                        Write-Verbose -Message $verboseMessage.NotUpToDate
                    }
                }
                ([GraniDonwloadCheckSumtype]::FileName.ToString()) {
                    # FileName only check : Is destination file exists or not.
                    Write-Debug -Message ($debugMessage.IsCheckSumFileName -f $CheckSum)
                    $returnHash.Ensure = [GraniDonwloadEnsuretype]::Present.ToString()
                }
            }
        }
    }
    catch {
        Write-Error $_
    }
    
    return $returnHash
}


function Set-TargetResource {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [System.String]$S3BucketName,

        [parameter(Mandatory = $false)]
        [System.String]$Key,

        [parameter(Mandatory = $true)]
        [System.String]$DestinationPath,

        [parameter(Mandatory = $false)]
        [System.String]$PreAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.String]$PostAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [parameter(Mandatory = $false)]
        [ValidateSet("FileHash", "FileName")]
        [System.String]$CheckSum = [GraniDonwloadCheckSumtype]::FileHash.ToString(),

        [parameter(Mandatory = $false)]
        [System.String]$Region = [string]::Empty
    )

    # validate S3 Bucket is exist
    ValidateS3Bucket -BucketName $S3BucketName -Region $Region
    ValidateS3Object -BucketName $S3BucketName -Key $Key -Region $Region

    # validate DestinationPath is valid
    ValidateFilePath -Path $DestinationPath

    # PreAction
    ExecuteScriptBlock -ScriptBlockString $PreAction -Credential $Credential

    # Start Download
    Write-Verbose $verboseMessage.StartS3Download
    ReadS3Object -BucketName $S3BucketName -Key $Key -File $DestinationPath -Region $Region

    # PostAction
    ExecuteScriptBlock -ScriptBlockString $PostAction -Credential $Credential
}


function Test-TargetResource {
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [System.String]$S3BucketName,

        [parameter(Mandatory = $false)]
        [System.String]$Key,

        [parameter(Mandatory = $true)]
        [System.String]$DestinationPath,

        [parameter(Mandatory = $false)]
        [System.String]$PreAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.String]$PostAction = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [parameter(Mandatory = $false)]
        [ValidateSet("FileHash", "FileName")]
        [System.String]$CheckSum = [GraniDonwloadCheckSumtype]::FileHash.ToString(),

        [parameter(Mandatory = $false)]
        [System.String]$Region = [string]::Empty
    )

    $param = @{
        S3BucketName    = $S3BucketName
        Key             = $Key
        DestinationPath = $DestinationPath
        PreAction       = $PreAction
        PostAction      = $PostAction
        CheckSum        = $CheckSum
        Region          = $Region
    }
    return (Get-TargetResource @param).Ensure -eq [GraniDonwloadEnsuretype]::Present.ToString()
}

#endregion

#region S3 Helper

# Test
function TestS3Bucket {
    [OutputType([Boolean])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $false)]
        [string]$Region
    )

    if ([string]::IsNullOrWhiteSpace($Region)) {
        return Test-S3Bucket -BucketName $BucketName
    }
    else {
        Write-Debug -Message ($debugMessage.OverrideRegion -f $Region)
        return Test-S3Bucket -BucketName $BucketName -Region $Region
    }
}
function TestS3Object {
    [OutputType([Boolean])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $true)]
        [string]$Key,

        [parameter(Mandatory = $false)]
        [string]$Region
    )
    
    Write-Debug -Message ($debugMessage.IsS3ObjectExist)
    if ([string]::IsNullOrWhiteSpace($Region)) {
        $objects = Get-S3Object -BucketName $BucketName
    }
    else {
        Write-Debug -Message ($debugMessage.OverrideRegion -f $Region)
        $objects = Get-S3Object -BucketName $BucketName -Region $Region
    }

    $result = $null
    $dic = New-Object "System.Collections.Generic.Dictionary[[string], [string]]"
    $objects | Foreach-Object { $dic.Add($_.Key, $_.Etag) }
    return $dic.TryGetValue($Key, [ref]$result)
}

# Validation
function ValidateS3Bucket {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $false)]
        [string]$Region
    )

    Write-Debug -Message ($debugMessage.ValidateS3Bucket -f $BucketName)
    if (-not (TestS3Bucket -BucketName $BucketName -Region $Region)) {
        throw New-Object System.NullReferenceException ($exceptionMessage.S3BucketNotExistEXception -f $BucketName)
    }
}

function ValidateS3Object {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $true)]
        [string]$Key,

        [parameter(Mandatory = $false)]
        [string]$Region
    )

    Write-Debug -Message ($debugMessage.ValidateS3Object -f $Key)
    if (-not (TestS3Object -BucketName $BucketName -Key $Key -Region $Region)) {
        throw New-Object System.NullReferenceException ($exceptionMessage.S3ObjectNotExistEXception -f $BucketName, $Key)
    }
}

function ValidateFilePath {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path
    )
    
    Write-Debug -Message ($debugMessage.ValidateFilePath -f $Path)
    $itemType = GetPathItemType -Path $Path
    switch ($itemType.ToString()) {
        ([GraniDonwloadItemTypeEx]::FileInfo.ToString()) {
            return;
        }
        ([GraniDonwloadItemTypeEx]::NotExists.ToString()) {
            # Create Parent Directory check
            $parentPath = Split-Path $Path -Parent
            if (-not (Test-Path -Path $parentPath)) {
                [System.IO.Directory]::CreateDirectory($parentPath) > $null
            }
        }
        Default {
            $errorId = "FileValidationFailure"
            $errorMessage = $exceptionMessage.DestinationPathAlreadyExistAsNotFile -f $Path, $itemType.ToString()
            ThrowInvalidDataException -ErrorId $errorId -ErrorMessage $errorMessage
        }
    }
}

# Hash
function GetFileHash {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path
    )

    return (Get-FileHash -Path $Path -Algorithm MD5).Hash
}

function GetS3ObjectHash {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $true)]
        [string]$Key,

        [parameter(Mandatory = $false)]
        [string]$Region
    )

    if ([string]::IsNullOrWhiteSpace($Region)) {
        return (Get-S3Object -BucketName $BucketName -Key $Key | Where-Object Key -eq $Key).ETag.Replace('"', "")
    }
    else {
        Write-Debug -Message ($debugMessage.OverrideRegion -f $Region)
        return (Get-S3Object -BucketName $BucketName -Key $Key -Region $Region | Where-Object Key -eq $Key).ETag.Replace('"', "")
    }
}

# Reader
function ReadS3Object {
    [OutputType([void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$BucketName,

        [parameter(Mandatory = $true)]
        [string]$Key,

        [parameter(Mandatory = $true)]
        [string]$File,

        [parameter(Mandatory = $false)]
        [string]$Region
    )

    if ([string]::IsNullOrWhiteSpace($Region)) {
        Read-S3Object -BucketName $BucketName -Key $Key -File $File
    }
    else {
        Write-Debug -Message ($debugMessage.OverrideRegion -f $Region)
        Read-S3Object -BucketName $BucketName -Key $Key -File $File -Region $Region
    }
}

#endregion

# ItemType Helper
function GetPathItemType {
    [OutputType([GraniDonwloadItemTypeEx])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("FullName", "LiteralPath", "PSPath")]
        [System.String]$Path = [string]::Empty
    )

    $type = [string]::Empty

    # Check type of the Path Item
    if (-not (Test-Path -Path $Path)) {
        return [GraniDonwloadItemTypeEx]::NotExists
    }
    
    $pathItem = Get-Item -Path $path
    $pathItemType = $pathItem.GetType().FullName
    $type = switch ($pathItemType) {
        "System.IO.FileInfo" {
            [GraniDonwloadItemTypeEx]::FileInfo
        }
        "System.IO.DirectoryInfo" {
            [GraniDonwloadItemTypeEx]::DirectoryInfo
        }
        Default {
            [GraniDonwloadItemTypeEx]::Other
        }
    }

    return $type
}

# ScriptBlock Execute Helper
function ExecuteScriptBlock {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $false)]
        [System.String]$ScriptBlockString = [string]::Empty,

        [parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty
    )

    if ($ScriptBlockString -eq [string]::Empty) { return; }

    try {
        $scriptBlock = [ScriptBlock]::Create($ScriptBlockString).GetNewClosure()
        if ($Credential -eq [PSCredential]::Empty) {
            Write-Debug ($debugMessage.ExecuteScriptBlock -f $ScriptBlockString)
            $scriptBlock.Invoke() | Out-String -Stream | Write-Debug
        }
        else {
            Write-Debug ($debugMessage.ExecuteScriptBlockWithCredential -f $ScriptBlockString)
            Invoke-Command -ScriptBlock $scriptBlock -Credential $Credential -ComputerName . | Out-String -Stream | Write-Debug
        }
    }
    catch {
        Write-Debug ($exceptionMessage.ScriptBlockException -f $ScriptBlockString)
        throw $_
    }
}

# Exception Helper
function ThrowInvalidDataException {
    [OutputType([Void])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [System.String]$ErrorId,

        [parameter(Mandatory = $true)]
        [System.String]$ErrorMessage
    )
    
    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData
    $exception = New-Object System.InvalidOperationException $ErrorMessage 
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $errorCategory, $null
    throw $errorRecord
}

Export-ModuleMember -Function *-TargetResource