DSCResources/MSFT_xRemoteFile/MSFT_xRemoteFile.psm1
# Import CommonResourceHelper $script:dscResourcesFolderFilePath = Split-Path -Path $PSScriptRoot -Parent $script:commonResourceHelperFilePath = Join-Path -Path $script:dscResourcesFolderFilePath -ChildPath 'CommonResourceHelper.psm1' Import-Module -Name $script:commonResourceHelperFilePath # Localized messages for verbose and error statements in this resource $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xRemoteFile' # Path where cache will be stored. It's cleared whenever LCM gets new configuration. $script:cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xRemoteFile" <# .SYNOPSIS The Get-TargetResource function is used to fetch the status of file specified in DestinationPath on the target machine. #> function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri ) # Check whether DestinationPath is existing file $ensure = 'Absent' $pathItemType = Get-PathItemType -Path $DestinationPath switch ($pathItemType) { 'File' { Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingFile -f $DestinationPath) $ensure = 'Present' } 'Directory' { Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingPath -f $DestinationPath) # If it's existing directory, let's check whether expectedDestinationPath exists $uriFileName = Split-Path -Path $Uri -Leaf $expectedDestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName if (Test-Path -Path $expectedDestinationPath) { Write-Verbose -Message ($script:localizedData.FileExistsInDestinationPath -f $uriFileName) $ensure = 'Present' } } 'Other' { Write-Verbose -Message ($script:localizedData.DestinationPathUnknownType -f $DestinationPath, $pathItemType) } 'NotExists' { Write-Verbose -Message ($script:localizedData.DestinationPathDoesNotExist -f $DestinationPath) } } return @{ DestinationPath = $DestinationPath Uri = $Uri Ensure = $ensure } } <# .SYNOPSIS The Set-TargetResource function is used to download file found under Uri location to DestinationPath Additional parameters can be specified to configure web request #> function Set-TargetResource { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri, [Parameter()] [System.String] $UserAgent, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $Headers, [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter()] [System.Boolean] $MatchSource = $true, [Parameter()] [System.Uint32] $TimeoutSec, [Parameter()] [System.String] $Proxy, [Parameter()] [System.Management.Automation.PSCredential] $ProxyCredential ) # Validate Uri if (-not (Test-UriScheme -Uri $Uri -Scheme 'http|https|file')) { $errorMessage = $script:localizedData.InvalidWebUriError -f $Uri New-InvalidDataException ` -ErrorId 'UriValidationFailure' ` -ErrorMessage $errorMessage } # Validate DestinationPath scheme if (-not (Test-UriScheme -Uri $DestinationPath -Scheme 'file')) { $errorMessage = $script:localizedData.InvalidDestinationPathSchemeError -f $DestinationPath New-InvalidDataException ` -ErrorId 'DestinationPathSchemeValidationFailure' ` -ErrorMessage $errorMessage } # Validate DestinationPath is not UNC path if ($DestinationPath.StartsWith('\\')) { $errorMessage = $script:localizedData.DestinationPathIsUncError -f $DestinationPath New-InvalidDataException ` -ErrorId 'DestinationPathIsUncFailure' ` -ErrorMessage $errorMessage } # Validate DestinationPath does not contain invalid characters @('*', '?', '"', '<', '>', '|') | Foreach-Object -Process { if ($DestinationPath.Contains($_)) { $errorMessage = $script:localizedData.DestinationPathHasInvalidCharactersError -f $DestinationPath New-InvalidDataException ` -ErrorId 'DestinationPathHasInvalidCharactersError' ` -ErrorMessage $errorMessage } } # Validate DestinationPath does not end with / or \ (Invoke-WebRequest requirement) if ($DestinationPath.EndsWith('/') -or $DestinationPath.EndsWith('\')) { $errorMessage = $script:localizedData.DestinationPathEndsWithInvalidCharacterError -f $DestinationPath New-InvalidDataException ` -ErrorId 'DestinationPathEndsWithInvalidCharacterError' ` -ErrorMessage $errorMessage } # Check whether DestinationPath's parent directory exists. Create if it doesn't. $destinationPathParent = Split-Path -Path $DestinationPath -Parent if (-not (Test-Path $destinationPathParent)) { $null = New-Item -ItemType Directory -Path $destinationPathParent -Force } # Check whether DestinationPath's leaf is an existing folder $uriFileName = Split-Path -Path $Uri -Leaf if (Test-Path $DestinationPath -PathType Container) { $DestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName } # Remove DestinationPath and MatchSource from parameters as they are not parameters of Invoke-WebRequest $null = $PSBoundParameters.Remove('DestinationPath') $null = $PSBoundParameters.Remove('MatchSource') # Convert headers to hashtable $null = $PSBoundParameters.Remove('Headers') $headersHashtable = $null if ($null -ne $Headers) { $headersHashtable = Convert-KeyValuePairArrayToHashtable -Array $Headers } # Invoke web request try { $currentProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' Write-Verbose -Message ($script:localizedData.DownloadingURI -f $DestinationPath, $URI) Invoke-WebRequest @PSBoundParameters -Headers $headersHashtable -OutFile $DestinationPath } catch [System.OutOfMemoryException] { $errorMessage = $script:localizedData.DownloadOutOfMemoryException -f $_ New-InvalidDataException ` -ErrorId 'SystemOutOfMemoryException' ` -ErrorMessage $errorMessage } catch [System.Exception] { $errorMessage = $script:localizedData.DownloadException -f $_ New-InvalidDataException ` -ErrorId 'SystemException' ` -ErrorMessage $errorMessage } finally { $ProgressPreference = $currentProgressPreference } # Update cache if (Test-Path -Path $DestinationPath) { $downloadedFile = Get-Item -Path $DestinationPath $lastWriteTime = $downloadedFile.LastWriteTimeUtc $filesize = $downloadedFile.Length $inputObject = @{} $inputObject['LastWriteTime'] = $lastWriteTime $inputObject['FileSize'] = $filesize Update-Cache -DestinationPath $DestinationPath -Uri $Uri -InputObject $inputObject } } <# .SYNOPSIS The Test-TargetResource function is used to validate if the DestinationPath exists on the machine. #> function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri, [Parameter()] [System.String] $UserAgent, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $Headers, [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter()] [System.Boolean] $MatchSource = $true, [Parameter()] [System.Uint32] $TimeoutSec, [Parameter()] [System.String] $Proxy, [Parameter()] [System.Management.Automation.PSCredential] $ProxyCredential ) # Check whether DestinationPath points to existing file or directory $fileExists = $false $uriFileName = Split-Path -Path $Uri -Leaf $pathItemType = Get-PathItemType -Path $DestinationPath switch ($pathItemType) { 'File' { Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingFile -f $DestinationPath) if ($MatchSource) { $file = Get-Item -Path $DestinationPath # Getting cache. It's cleared every time user runs Start-DscConfiguration $cache = Get-Cache -DestinationPath $DestinationPath -Uri $Uri if ($null -ne $cache ` -and ($cache.LastWriteTime -eq $file.LastWriteTimeUtc) ` -and ($cache.FileSize -eq $file.Length)) { Write-Verbose -Message $script:localizedData.CacheReflectsCurrentState $fileExists = $true } else { Write-Verbose -Message $script:localizedData.CacheIsEmptyOrNotMatchCurrentState } } else { Write-Verbose -Message $script:localizedData.MatchSourceFalse $fileExists = $true } } 'Directory' { Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingPath -f $DestinationPath) $expectedDestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName if (Test-Path -Path $expectedDestinationPath) { if ($MatchSource) { $file = Get-Item -Path $expectedDestinationPath $cache = Get-Cache -DestinationPath $expectedDestinationPath -Uri $Uri if ($null -ne $cache -and ($cache.LastWriteTime -eq $file.LastWriteTimeUtc)) { Write-Verbose -Message $script:localizedData.CacheReflectsCurrentState $fileExists = $true } else { Write-Verbose -Message $script:localizedData.CacheIsEmptyOrNotMatchCurrentState } } else { Write-Verbose -Message $script:localizedData.MatchSourceFalse $fileExists = $true } } } 'Other' { Write-Verbose -Message ($script:localizedData.DestinationPathUnknownType -f $DestinationPath, $pathItemType) } 'NotExists' { Write-Verbose -Message ($script:localizedData.DestinationPathDoesNotExist -f $DestinationPath) } } $result = $fileExists return $result } <# .SYNOPSIS Throws terminating error of category InvalidData with specified errorId and errorMessage #> function New-InvalidDataException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [System.String] $ErrorMessage ) $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData $exception = New-Object ` -TypeName System.InvalidOperationException ` -ArgumentList $ErrorMessage $errorRecord = New-Object ` -TypeName System.Management.Automation.ErrorRecord ` -ArgumentList $exception, $ErrorId, $errorCategory, $null throw $errorRecord } <# .SYNOPSIS Checks whether given URI represents specific scheme .DESCRIPTION Most common schemes: file, http, https, ftp We can also specify logical expressions like: [http|https] #> function Test-UriScheme { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Uri, [Parameter(Mandatory = $true)] [System.String] $Scheme ) $newUri = $Uri -as [System.URI] return ($null -ne $newUri.AbsoluteURI -and $newUri.Scheme -match $Scheme) } <# .SYNOPSIS Gets type of the item which path points to. .OUTPUTS File, Directory, Other or NotExists #> function Get-PathItemType { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $Path ) $type = $null # Check whether path exists if (Test-Path $path) { # Check type of the path $pathItem = Get-Item -Path $Path $pathItemType = $pathItem.GetType().Name if ($pathItemType -eq 'FileInfo') { $type = 'File' } elseif ($pathItemType -eq 'DirectoryInfo') { $type = 'Directory' } else { $type = 'Other' } } else { $type = 'NotExists' } return $type } <# .SYNOPSIS Converts CimInstance array of type KeyValuePair to hashtable #> function Convert-KeyValuePairArrayToHashtable { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [Microsoft.Management.Infrastructure.CimInstance[]] $Array ) $hashtable = @{} foreach ($item in $Array) { $hashtable += @{ $item.Key = $item.Value } } return $hashtable } <# .SYNOPSIS Gets cache for specific DestinationPath and Uri #> function Get-Cache { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri ) $cacheContent = $null $key = Get-CacheKey -DestinationPath $DestinationPath -Uri $Uri $path = Join-Path -Path $script:cacheLocation -ChildPath $key Write-Verbose -Message ($script:localizedData.CacheLookingForPath -f $Path) if (-not (Test-Path -Path $path)) { Write-Verbose -Message ($script:localizedData.CacheNotFoundForPath -f $DestinationPath, $Uri, $Key) $cacheContent = $null } else { $cacheContent = Import-CliXml -Path $path Write-Verbose -Message ($script:localizedData.CacheFoundForPath -f $DestinationPath, $Uri, $Key) } return $cacheContent } <# .SYNOPSIS Creates or updates cache for specific DestinationPath and Uri #> function Update-Cache { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri, [Parameter(Mandatory = $true)] [System.Object] $InputObject ) $key = Get-CacheKey -DestinationPath $DestinationPath -Uri $Uri $path = Join-Path -Path $script:cacheLocation -ChildPath $key if (-not (Test-Path -Path $script:cacheLocation)) { $null = New-Item -ItemType Directory -Path $script:cacheLocation } Write-Verbose -Message ($script:localizedData.UpdatingCache -f $DestinationPath, $Uri, $Key) Export-CliXml -Path $path -InputObject $InputObject -Force } <# .SYNOPSIS Returns cache key for given parameters #> function Get-CacheKey { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Uri ) return [System.String]::Join('', @($DestinationPath, $Uri)).GetHashCode().ToString() } Export-ModuleMember -Function Get-TargetResource, Set-TargetResource, Test-TargetResource |