DSCResources/Grani_GitHubApiContent/Grani_GitHubApiContent.psm1

#region Initialize

function Initialize
{
    # Load Assembly to use HttpClient
    try
    {
        Add-Type -AssemblyName System.Net.Http
    }
    catch
    {
    }

    # cache Location Variable
    # MSFT using this path, but this always clear when LCM runs. It means whenever you run "Get" you can't refer cache.
    # => need to change to persistence path to match with cache.
    # $script:cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\Grani_Download"
    $script:cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\CustomProvCache\Grani_GitHubApiContent"

    # GitHub API template string : RepositoryOwner / Repository / ContentPath / Branch
    $script:githubApiString = "https://api.github.com/repos/{0}/{1}/contents/{2}?ref={3}"

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

}

Initialize

#endregion

#region Message Definition

$debugMessage = DATA {
    ConvertFrom-StringData -StringData "
        AddRequestHeader = Adding Request Header. Key : '{0}', Value : '{1}'
        AddContentType = Adding ContentType : '{0}'
        AddKeepAliveToRequestHeader = Adding Keep-Alive as true to the Request Header.
        AddOAuth2Token = Adding OAuth2Token for Basic Authentication.
        AddUserAgent = Adding UserAgent : '{0}'
        ContentTypeDetectedAsJson = ContentType passed to Request header as '{0}'. Treat response as JSON, content encoded as base64.
        ContentTypeDetectedAsRaw = ContentType passed to Request header as '{0}'. Treat response as RAW, content directly pick from result.
        ContentTypeDetectedAsHtml = ContentType passed to Request header as '{0}'. Treat response as HTML, content directly pick from result.
        ConvertBase64String = Convert base64 string to UTF8 string.
        ConvertstringToJsonAndGetContent = Convert json string to PSCustomObject, then picking up base64 encoded string from content property.
        DownloadComplete = Download content complete.
        GetRawContent = Getting raw content from response result
        IsDestinationPathExist = Checking Destination Path is existing and Valid as a FileInfo
        IsDestinationPathAlreadyUpToDate = Matching FileHash to verify file is already exist/Up-To-Date or not.
        IsFileAlreadyUpToDate = CurrentFileHash : CachedFileHash -> {0} : {1}
        IsFileExists = File found from DestinationPath. Checking already up-to-date.
        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}'
        SetCacheLocationPath = CacheLocation Value detected. Setting Custom CacheLocation Path : '{0}'
        TestUriConnection = Testing connection to the URI : {0}
        UpdateFileHashCache = Updating cache path '{1}' for current File hash SHA256 '{0}'.
        ValidateUri = Cast URI string '{0}' to System.Uri.
        ValidateFilePath = Check DestinationPath '{0}' is FileInfo and Parent Directory already exist.
        WriteStream = Start writing downloaded string to File Path : '{0}'
    "

}

$verboseMessage = DATA {
    ConvertFrom-StringData -StringData "
        alreadyUpToDate = Current DestinationPath FileHash and Cache FileHash matched. File already Up-To-Date.
        DownloadStream = Status Code returns '{0}'. Start download stream from URI : '{1}'
        DownloadString = Status Code returns '{0}'. Start download string from URI : '{1}'
        notUpToDate = Current DestinationPath FileHash and Cache FileHash not matched. Need to download latest file.
    "

}
$exceptionMessage = DATA {
    ConvertFrom-StringData -StringData "
        ContentNotFoundFromResponce = Content not exist in GitHub API response json string.
        ResultNotFountFromResponce = Result not exist in GitHub API response raw string.
        InvalidCastURI = Uri : '{0}' casted to [System.Uri] but was invalid string for URI. Make sure you have passed valid URI string.
        InvalidUriSchema = Specified URI is not valid: '{0}'. Only https is accepted.
        InvalidResponce = Status Code returns '{0}'. Stop download content from URI : '{1}'
        DestinationPathAlreadyExistAsNotFile = Destination Path '{0}' already exist but not a file. Found itemType is {1}. Windows not allowed exist same name item.
    "

}

#endregion

#region *-TargetResource

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

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

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

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

        [parameter(Mandatory = $false)]
        [System.String]$Branch = "master",

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

        [parameter(Mandatory = $false)]
        [Microsoft.Management.Infrastructure.CimInstance[]]$Header = $null,

        [parameter(Mandatory = $false)]
        [ValidateSet("application/json","application/vnd.github+json","application/vnd.github.v3.raw","application/vnd.github.v3.html")]
        [System.String]$ContentType = "application/json",

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer,

        [parameter(Mandatory = $false)]
        [System.Boolean]$AllowRedirect = $true,

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

    # Set Custom Cache Location
    if ($CacheLocation -ne [string]::Empty)
    {
        Write-Debug -Message ($debugMessage.SetCacheLocationPath -f $CacheLocation)
        $script:cacheLocation = $CacheLocation
    }

    # Setup GitHub API Uri string from GitHub parameters
    $uri = ParseGitHubApiUri -RepositoryOwner $RepositoryOwner -Repository $Repository -ContentPath $ContentPath -Branch $Branch

    # validate Uri can be parse to [URI] and Schema is http|https|file
    $validUri = ValidateUri -Uri $uri

    # Initialize return values
    # Header and OAuth2Token will never return as TypeConversion problem
    $returnHash = 
    @{
        DestinationPath = $DestinationPath
        Repository = $Repository
        RepositoryOwner = $RepositoryOwner
        ContentPath = $ContentPath
        Branch = $Branch
        ContentType = $ContentType
        UserAgent = $UserAgent
        AllowRedirect = $AllowRedirect
        OAuth2Token = New-CimInstance -ClassName MSFT_Credential -Property @{Username=[string]$OAuth2Token.UserName; Password=[string]$null} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly
        CacheLocation = $CacheLocation
        Ensure = "Absent"
    }

    # Destination Path check
    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)
        }
        ([GraniDonwloadItemTypeEx]::NotExists.ToString())
        {
            Write-Debug -Message ($debugMessage.ItemTypeWasNotExists -f $DestinationPath)
        }
    }

    # Already Up-to-date Check
    Write-Debug -Message $debugMessage.IsDestinationPathAlreadyUpToDate
    if ($fileExists -eq $true)
    {
        Write-Debug -Message $debugMessage.IsFileExists
        $currentFileHash = GetFileHash -Path $DestinationPath
        $cachedFileHash = GetCache -DestinationPath $DestinationPath -Uri $validUri

        Write-Debug -Message ($debugMessage.IsFileAlreadyUpToDate -f $currentFileHash, $cachedFileHash)
        if ($currentFileHash -eq $cachedFileHash)
        {
            Write-Verbose -Message $verboseMessage.alreadyUpToDate
            $returnHash.Ensure = "Present"
        }
        else
        {
            Write-Verbose -Message $verboseMessage.notUpToDate
        }
    }

    return $returnHash
}


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

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

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

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

        [parameter(Mandatory = $false)]
        [System.String]$Branch = "master",

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

        [parameter(Mandatory = $false)]
        [Microsoft.Management.Infrastructure.CimInstance[]]$Header = $null,

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("application/json","application/vnd.github+json","application/vnd.github.v3.raw","application/vnd.github.v3.html")]
        [System.String]$ContentType = "application/json",

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer,

        [parameter(Mandatory = $false)]
        [System.Boolean]$AllowRedirect = $true,

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

    # Set Custom Cache Location
    if ($CacheLocation -ne [string]::Empty)
    {
        Write-Debug -Message ($debugMessage.SetCacheLocationPath -f $CacheLocation)
        $script:cacheLocation = $CacheLocation
    }

    # Setup GitHub API Uri string from GitHub parameters
    $uri = ParseGitHubApiUri -Repository $Repository -RepositoryOwner $RepositoryOwner -ContentPath $ContentPath -Branch $Branch

    # validate Uri can be parse to [URI] and Schema is http|https|file
    $validUri = ValidateUri -Uri $Uri

    # validate DestinationPath is valid
    ValidateFilePath -Path $DestinationPath

    # Convert CimInstance to HashTable
    $headerHashtable = ConvertKCimInstanceToHashtable -CimInstance $Header

    # Start Download
    Invoke-HttpClient -Uri $validUri -Path $DestinationPath -Header $headerHashtable -ContentType $ContentType -UserAgent $UserAgent -OAuth2Token $OAuth2Token -AllowRedirect $AllowRedirect

    # Update Cache for FileHash
    UpdateCache -DestinationPath $DestinationPath -Uri $validUri
}


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

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

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

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

        [parameter(Mandatory = $false)]
        [System.String]$Branch = "master",

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

        [parameter(Mandatory = $false)]
        [Microsoft.Management.Infrastructure.CimInstance[]]$Header = $null,

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("application/json","application/vnd.github+json","application/vnd.github.v3.raw","application/vnd.github.v3.html")]
        [System.String]$ContentType = "application/json",

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer,

        [parameter(Mandatory = $false)]
        [System.Boolean]$AllowRedirect = $true,

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

    $param = @{
        DestinationPath = $DestinationPath
        RepositoryOwner = $RepositoryOwner
        Repository = $Repository
        ContentPath = $ContentPath
        Branch = $Branch
        OAuth2Token = $OAuth2Token
        Header = $Header
        ContentType = $ContentType
        UserAgent = $UserAgent
        AllowRedirect = $AllowRedirect
        CacheLocation = $CacheLocation
    }
    return (Get-TargetResource @param).Ensure -eq "Present"
}


#endregion

#region HttpClient Helper

function Invoke-HttpClient
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [uri]$Uri,

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

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

        [parameter(Mandatory = $false)]
        [System.Collections.Hashtable]$Header = @{},

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("application/json","application/vnd.github+json","application/vnd.github.v3.raw","application/vnd.github.v3.html")]
        [System.String]$ContentType = "application/json",

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer,

        [parameter(Mandatory = $false)]
        [System.Boolean]$AllowRedirect = $true
    )

    begin
    {
        #region Initialize

        # Should support Timeout? : Default -> 1:40 min
        # Should support MaxResponseContentBufferSize? : Default -> 2147483647

        $httpClientHandler = New-Object System.Net.Http.HttpClientHandler
        $httpClientHandler.AllowAutoRedirect = $AllowRedirect
        
        $httpClient = New-Object System.Net.Http.HttpClient ($httpClientHandler)

        # Request Header
        if ($Header.Keys.Count -ne 0)
        {
            foreach ($item in $Header.GetEnumerator())
            {
                Write-Debug -Message ($debugMessage.AddRequestHeader -f $item.Key, $item.Value)
                $httpClient.DefaultRequestHeaders.Add($item.Key, $item.Value)
            }   
        }

        # Request Header : Keep-Alive
        if (($httpClient.DefaultRequestHeaders.GetEnumerator() | where Key -eq "Keep-Alive" | measure).Count -eq 0)
        {
            Write-Debug -Message ($debugMessage.AddKeepAliveToRequestHeader)
            $httpClient.DefaultRequestHeaders.Add("Keep-Alive", "true")
        }

        # ContentType
        if ($ContentType -ne [string]::Empty)
        {
            Write-Debug -Message ($debugMessage.AddContentType -f $ContentType)
            $private:mediaType = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue($ContentType)
            $httpClient.DefaultRequestHeaders.Accept.Add($mediaType)
        }

        # UserAgent
        if ($UserAgent -ne [string]::Empty)
        {
            Write-Debug -Message ($debugMessage.AddUserAgent -f $UserAgent)
            $httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($UserAgent)
        }

        # Credential
        if ($OAuth2Token.GetNetworkCredential().Password -ne [string]::Empty)
        {
            # Credential on Handler does not work with Basic Authentication : http://stackoverflow.com/questions/25761214/why-would-my-rest-service-net-clients-send-every-request-without-authentication
            # $httpClientHandler.Credential = $Credential

            $private:authorizationHeaderKey = "Authorization"
            $private:authorizationHeaderValue = "token {0}" -f $OAuth2Token.GetNetworkCredential().Password

            Write-Debug -Message ($debugMessage.AddOAuth2Token)
            $httpClient.DefaultRequestHeaders.Add($private:authorizationHeaderKey, $private:authorizationHeaderValue) # Basic Authentication Only
        }

        #endregion
    }

    end
    {
        try
        {
            #region Test Connection

            Write-Debug -Message ($debugMessage.TestUriConnection -f $Uri.ToString())
            $res = $httpClient.GetAsync($Uri)
            $res.ConfigureAwait($false) > $null
            if ($res.Exception -ne $null){ throw $res.Exception }
            if ($res.Result.StatusCode -ne [System.Net.HttpStatusCode]::OK){ throw ($exceptionMessage.InvalidResponce -f $res.Result.StatusCode.value__, $Uri) }
            
            #endregion

            #region Execute Download

            switch ($ContentType)
            {
                "application/json"
                {
                    Write-Verbose -Message ($verboseMessage.DownloadString -f $res.Result.StatusCode.value__, $Uri)
                    [System.Threading.Tasks.Task`1[string]]$requestResult = GetStringAsync -Uri $Uri

                    Write-Debug -Message $debugMessage.ContentTypeDetectedAsJson
                    WriteJson -Path $Path -RequestResult $requestResult
                }
                "application/vnd.github+json"
                {
                    Write-Verbose -Message ($verboseMessage.DownloadString -f $res.Result.StatusCode.value__, $Uri)
                    [System.Threading.Tasks.Task`1[string]]$requestResult = GetStringAsync -Uri $Uri
                    
                    Write-Debug -Message $debugMessage.ContentTypeDetectedAsJson
                    WriteJson -Path $Path -RequestResult $requestResult
                }
                "application/vnd.github.v3.raw"
                {
                    Write-Verbose -Message ($verboseMessage.DownloadStream -f $res.Result.StatusCode.value__, $Uri)
                    [System.Threading.Tasks.Task`1[System.IO.Stream]]$stream = GetStreamAsync -Uri $Uri

                    Write-Debug -Message $debugMessage.ContentTypeDetectedAsRaw
                    WriteStream -Path $Path -Stream $stream
                }
                "application/vnd.github.v3.html"
                {
                    Write-Verbose -Message ($verboseMessage.DownloadStream -f $res.Result.StatusCode.value__, $Uri)
                    [System.Threading.Tasks.Task`1[System.IO.Stream]]$stream = GetStreamAsync -Uri $Uri

                    Write-Debug -Message $debugMessage.ContentTypeDetectedAsHtml
                    WriteStream -Path $Path -Stream $stream
                }
            }
                        
            #endregion
            
            Write-Verbose -Message ($debugMessage.DownloadComplete)
        }
        catch [System.Exception]
        {
            throw $_
        }
        finally
        {
            if (($null -ne $res) -and ($res.IsCompleted -eq $true)){ $res.Dispose() }
            if ($null -ne $httpClient){ $httpClient.Dispose() }
            if ($null -ne $httpClientHandler){ $httpClientHandler.Dispose() }
        }
    }
}

function GetStringAsync
{
    [OutputType([System.Threading.Tasks.Task`1[string]])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [uri]$Uri
    )

    [System.Threading.Tasks.Task`1[string]]$requestResult = $httpClient.GetStringAsync($Uri)
    $requestResult.ConfigureAwait($false) > $null
    if ($requestResult.Exception -ne $null){ throw $requestResult.Exception }
    return $requestResult
}

function GetStreamAsync
{
    [OutputType([System.Threading.Tasks.Task`1[System.IO.Stream]])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [uri]$Uri
    )

    [System.Threading.Tasks.Task`1[System.IO.Stream]]$stream = $httpClient.GetStreamAsync($Uri)
    $stream.ConfigureAwait($false) > $null
    if ($stream.Exception -ne $null){ throw $stream.Exception }
    return $stream
}

function WriteJson
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$Path,

        [parameter(Mandatory = $true)]
        [System.Threading.Tasks.Task`1[string]]$RequestResult
    )

    begin
    {
        function GetContentBase64String
        {
            [OutputType([string])]
            [CmdletBinding()]
            param
            (
                [parameter(Mandatory = $true)]
                [System.Threading.Tasks.Task`1[string]]$RequestResult
            )

            # Get Content base64 string
            Write-Debug -Message $debugMessage.ConvertstringToJsonAndGetContent
            $content = ($RequestResult.Result | ConvertFrom-Json).Content

            if ($content -eq [string]::Empty){ throw New-Object System.NullReferenceException $exceptionMessage.ContentNotFoundFromResponce }
            return $content
        }

        function ConvertFromBase64ToUTF8
        {
            [OutputType([string])]
            [CmdletBinding()]
            param
            (
                [parameter(Mandatory = $true)]
                [string]$String
            )

            try
            {
                # convert bse64 to UTF8
                Write-Debug -Message $debugMessage.ConvertBase64String
                $utf8String = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($String))
                return $utf8String
            }
            catch
            {
                throw $_
            }
        }
    }

    process
    {
        try
        {
            # Get base64 string from Content Property of response json
            $content = GetContentBase64String -RequestResult $RequestResult
        
            # decode base64 to utf8 string
            $decodedString = ConvertFromBase64ToUTF8 -String $Content

            # Write content to the file.
            [System.IO.File]::WriteAllText($Path, $decodedString, [System.Text.Encoding]::UTF8)
            
        }
        finally
        {
            if (($null -ne $RequestResult) -and ($RequestResult.IsCompleted -eq $true)){ $RequestResult.Dispose() }
        }
    }
}

function WriteStream
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$Path,

        [parameter(Mandatory = $true)]
        [System.Threading.Tasks.Task`1[System.IO.Stream]]$Stream
    )

    try
    {
        # Write stream to the File
        Write-Debug -Message ($debugMessage.WriteStream -f $Path)
        $fileStream = [System.IO.File]::Create($Path)
        $Stream.Result.CopyTo($fileStream)
    }
    finally
    {
        if ($null -ne $fileStream){ $fileStream.Dispose() }
        if (($null -ne $Stream) -and ($Stream.IsCompleted -eq $true)){ $Stream.Dispose() }
    }
}

#endregion

#region Parse Helper

function ParseGitHubApiUri
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]$RepositoryOwner,

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

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

        [parameter(Mandatory = $false)]
        [System.String]$Branch = "master"
    )

    $uriString = $script:githubApiString -f $RepositoryOwner, $Repository, $ContentPath, $Branch
    return $uriString
}

#endregion

#region Validation Helper

function ValidateUri
{
    [OutputType([uri])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$Uri
    )
    
    Write-Debug -Message ($debugMessage.ValidateUri -f $Uri)
    [uri]$result = $Uri -as [uri]
    if ($result.AbsolutePath -eq $null){ throw New-Object System.NullReferenceException ($exceptionMessage.InvalidCastURI -f $Uri)}
    if ($result.Scheme -ne "https")
    {
        $errorId = "UriValidationFailure";
        $errorMessage = $exceptionMessage.InvalidUriSchema -f ${Uri}
        ThrowInvalidDataException -ErrorId $errorId -ErrorMessage $errorMessage
    }
    return $result
}

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
        }
    }

}

#endregion

#region Cache Helper

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

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

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

        [parameter(Mandatory = $true)]
        [uri]$Uri
    )

    $key = [string]::Join("", @($DestinationPath, $Uri.AbsoluteUri.ToString())).GetHashCode().ToString()
    return $key
}

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

        [parameter(Mandatory = $true)]
        [uri]$Uri
    )

    $cacheKey = GetCacheKey -DestinationPath $DestinationPath -Uri $Uri
    $path = Join-Path $script:cacheLocation $cacheKey
    
    # Test Cache Path is exist
    if (-not (Test-Path -Path $path)){ return [string]::Empty }

    # Get FileHash from Cache File
    $fileHash = (Import-CliXml -Path $path).FileHash
    return $fileHash    
}

function UpdateCache
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$DestinationPath,

        [parameter(Mandatory = $true)]
        [uri]$Uri
    )

    $cacheKey = GetCacheKey -DestinationPath $DestinationPath -Uri $Uri
    $path = Join-Path $script:cacheLocation $cacheKey

    # create cacheLocaltion Directory
    if (-not (Test-Path -Path $script:cacheLocation))
    {
        [System.IO.Directory]::CreateDirectory($script:cacheLocation) > $null
    }

    # Create Cache Object
    $fileHash = GetFileHash -Path $DestinationPath
    $obj = NewXmlObject -DestinationPath $DestinationPath -Uri $Uri -FileHash $fileHash

    # export cache to CliXML
    Write-Debug ($debugMessage.UpdateFileHashCache -f $fileHash, $Path)
    $obj | Export-CliXml -Path $path -Force
}

function NewXmlObject
{
    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$DestinationPath,

        [parameter(Mandatory = $true)]
        [uri]$Uri,

        [parameter(Mandatory = $true)]
        [string]$FileHash
    )
    
    $obj = @{}
    $obj.FileHash = $FileHash
    $obj.WriteTime = [System.IO.File]::GetLastWriteTimeUtc($DestinationPath)
    $obj.Path = $DestinationPath
    $obj.Uri = $Uri.AbsoluteUri.ToString()
    return [PSCustomObject]$obj
}

#endregion

#region 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
}

#endregion

#region Converter from Microsoft.Management.Infrastructure.CimInstance[] (KeyValuePair) to HashTable

function ConvertKCimInstanceToHashtable
{
    [OutputType([hashtable[]])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $false)]
        [AllowNull()]
        [Microsoft.Management.Infrastructure.CimInstance[]]$CimInstance
    )

    if ($null -eq $CimInstance)
    {
        return @{}
    }

    $hashtable = New-Object System.Collections.Generic.List[hashtable]
    foreach($item in $CimInstance.GetEnumerator())
    {
        $hashtable.Add(@{$item.Key = $item.Value})
    }

    return $hashtable
}

#endregion

#region 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
}

#endregion

Export-ModuleMember -Function *-TargetResource