GitHubRepository.ps1
$localized = data { ConvertFrom-StringData @' ResolvedDestinationPath = Resolved destination path '{0}'. ResolvedSourcePath = Resolved source path '{0}'. ExpandingZipArchive = Expanding Zip archive '{0}'. CreatingDirectory = Creating target directory '{0}'. ExtractingZipArchiveEntry = Extracting Zip archive entry '{0}'. ClosingZipArchive = Closing Zip archive '{0}'. CleaningRepositoryDirectory = Cleaning repository directory '{0}'. TargetFileExistsWarning = Target file '{0}' already exists. InvalidDestinationPathError = Invalid destination path '{0}' specified. '@ } function ExpandZipArchive { <# .SYNOPSIS Extracts a GitHub Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] [OutputType([System.IO.FileInfo])] param ( # Source path to the Zip Archive. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [ValidateNotNullOrEmpty()] [Alias('PSPath','FullName')] [System.String[]] $Path, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, # GitHub repository name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Repository, # GitHub repository branch name [Parameter(ValueFromPipelineByPropertyName, Position = 2)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master', [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepository, # Overwrite existing files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## Remove root folders/files in archive from destination path. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Clean ) begin { ## Validate destination path if (-not (Test-Path -Path $DestinationPath -IsValid)) { throw ($localized.InvalidDestinationPathError -f $DestinationPath); } $DestinationPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath); Write-Verbose ($localized.ResolvedDestinationPath -f $DestinationPath); [Ref] $null = NewDirectory -Path $DestinationPath; foreach ($pathItem in $Path) { foreach ($resolvedPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathItem)) { Write-Verbose ($localized.ResolvedSourcePath -f $resolvedPath); $LiteralPath += $resolvedPath; } } ## If all tests passed, load the required .NET assemblies Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } # end begin process { if ($Clean) { ## Remove repository directory before expanding any items.. $repositoryPath = Join-Path -Path $DestinationPath -ChildPath $Repository; if ($OverrideRepository) { $repositoryPath = Join-Path -Path $DestinationPath -ChildPath $OverrideRepository; } Write-Verbose ($localized.CleaningRepositoryDirectory -f $repositoryPath); if (Test-Path -Path $repositoryPath -PathType Container) { Remove-Item -Path $repositoryPath -Force -Recurse -ErrorAction Stop; } } foreach ($pathEntry in $LiteralPath) { try { Write-Verbose ($localized.ExpandingZipArchive -f $pathEntry); $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($pathEntry); $expandZipArchiveItemParams = @{ InputObject = [ref] $zipArchive.Entries; DestinationPath = $DestinationPath; Repository = $Repository; Branch = $Branch; Force = $Force; } if ($OverrideRepository) { $expandZipArchiveItemParams['OverrideRepository'] = $OverrideRepository; } ExpandZipArchiveItem @expandZipArchiveItemParams; } # end try catch { Write-Error $_.Exception; } finally { ## Close the file handle CloseZipArchive; } } # end foreach } # end process } #end function ExpandZipArchive function ExpandZipArchiveItem { <# .SYNOPSIS Extracts file(s) from a GitHub Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([System.IO.FileInfo])] param ( # Reference to Zip archive item. [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'InputObject')] [ValidateNotNullOrEmpty()] [System.IO.Compression.ZipArchiveEntry[]] [Ref] $InputObject, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, # GitHub repository name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, # GitHub repository branch name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master', ## Override repository name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepository, # Overwrite existing physical filesystem files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } process { try { ## Regex for locating the <RepositoryName>-<Branch>\ root directory $searchString = '^{0}-{1}\\' -f $Repository, $Branch; $replacementString = '{0}\' -f $Repository; if ($OverrideRepository) { $replacementString = '{0}\' -f $OverrideRepository; } foreach ($zipArchiveEntry in $InputObject) { if ($zipArchiveEntry.FullName.Contains('/')) { ## We need to create the directory path as the ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\ $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\'); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = NewDirectory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } # end if else { ## Just a file in the root so just use the $DestinationPath $fullDestinationFilePath = Join-Path -Path $DestinationPath -ChildPath $zipArchiveEntry.Name; } # end else if ([System.String]::IsNullOrEmpty($zipArchiveEntry.Name)) { ## This is a folder and we need to create the directory path as the ## ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\ $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\'); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = NewDirectory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } elseif (-not $Force -and (Test-Path -Path $fullDestinationFilePath -PathType Leaf)) { ## Are we overwriting existing files (-Force)? Write-Warning ($localized.TargetFileExistsWarning -f $fullDestinationFilePath); } else { ## Just overwrite any existing file if ($Force -or $PSCmdlet.ShouldProcess($fullDestinationFilePath, 'Expand')) { Write-Debug ($localized.ExtractingZipArchiveEntry -f $fullDestinationFilePath); [System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipArchiveEntry, $fullDestinationFilePath, $true); ## Return a FileInfo object to the pipline Write-Output (Get-Item -Path $fullDestinationFilePath); } } # end if } # end foreach zipArchiveEntry } # end try catch { Write-Error $_.Exception; } } # end process } #end function ExpandZipArchiveItem function CloseZipArchive { <# .SYNOPSIS Tidies up and closes Zip Archive and file handles #> [CmdletBinding()] param () process { Write-Verbose ($localized.ClosingZipArchive -f $Path); if ($null -ne $zipArchive) { $zipArchive.Dispose(); } if ($null -ne $fileStream) { $fileStream.Close(); } } # end process } #end function CloseZipArchive function NewDirectory { <# .SYNOPSIS Creates a file system directory. .DESCRIPTION The New-Directory cmdlet will create the target directory if it doesn't already exist. If the target path already exists, the cmdlet does nothing. .INPUTS You can pipe multiple strings or multiple System.IO.DirectoryInfo objects to this cmdlet. .OUTPUTS System.IO.DirectoryInfo .NOTES This is an internal function and should not be called directly. #> [CmdletBinding(DefaultParameterSetName = 'ByString', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([System.IO.DirectoryInfo])] param ( # Target filesystem directory to create [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByDirectoryInfo')] [ValidateNotNullOrEmpty()] [System.IO.DirectoryInfo[]] $InputObject, # Target filesystem directory to create [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByString')] [ValidateNotNullOrEmpty()] [Alias('PSPath')] [System.String[]] $Path ) begin { Write-Debug ('Using parameter set ''{0}''.' -f $PSCmdlet.ParameterSetName); } process { switch ($PSCmdlet.ParameterSetName) { 'ByString' { foreach ($directoryPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)) { Write-Debug ('Testing target directory ''{0}''.' -f $directoryPath); if (-not (Test-Path -Path $directoryPath -PathType Container)) { if ($PSCmdlet.ShouldProcess($directoryPath, 'Create')) { Write-Verbose ($localized.CreatingDirectory -f $directoryPath); Write-Output (New-Item -Path $directoryPath -ItemType Directory); } } else { Write-Debug ('Target directory ''{0}'' already exists.' -f $Directory); Write-Output (Get-Item -Path $directoryPath); } } # end foreach } # end ByString 'ByDirectoryInfo' { foreach ($directoryInfo in $InputObject) { Write-Debug ('Testing target directory ''{0}''.' -f $directoryInfo.FullName); if (-not ($directoryInfo.Exists)) { if ($PSCmdlet.ShouldProcess($directoryInfo.FullName, 'Create')) { Write-Verbose ($localized.CreatingDirectory -f $directoryInfo.FullName); Write-Output (New-Item -Path $directoryInfo.FullName -ItemType Directory); } } else { Write-Debug ('Target directory ''{0}'' already exists.' -f $directoryInfo.FullName); Write-Output $directoryInfo; } } # end foreach } #end ByDirectoryInfo } #end switch } # end process } #end function NewDirectory function ResolveGitHubUri { <# .SYNOPSIS Resolves the correct GitHub URI for the specified Owner, Repository and Branch. #> [CmdletBinding()] [OutputType([System.Uri])] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Owner, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master' ) process { $uri = 'https://github.com/{0}/{1}/archive/{2}.zip' -f $Owner, $Repository, $Branch; return New-Object -TypeName System.Uri -ArgumentList $uri; } #end process } #end function ResolveGitHubUri function Install-GitHubRepository { <# .SYNOPSIS Downloads, extracts and installs a repository directly from GitHub. .DESCRIPTION The Install-GitHubRepository cmdlet will download and extract a GitHub repository. This will typically be development PowerShell modules or DSC resources. Install-GitHubRepository is primary intended to help bootstrap the installation of Powershell modules and DSC resources that have not (yet) been published to the PowerShell Gallery or have been updated on a development branch and are needed for testing purposes. .PARAMETER Owner Specifies the owner of the GitHub repository from whom to download the module. .PARAMETER Repository Specifies the GitHub repository name to download. .PARAMETER Branch Specifies the specific Git repository branch to download. If this is not specified it defaults to the 'master' branch. .PARAMETER DestinationPath Specifies the path to the folder in which you want the command to save GitHub repository. Enter the path to a folder, but do not specify a file name or file name extension. If this parameter is not specified, it defaults to the '$env:ProgramFiles\WindowsPowershell\Modules' directory. .PARAMETER OverrideRepository Specifies overriding the repository name when it's expanded to disk. Use this parameter when the extracted Zip file path does not meet your requirements, i.e. when the repository name does not match the Powershell module name. .PARAMETER Force Forces the extraction of files from an archive file. By default, any files that exist on the local file system are not overwritten. .PARAMETER Clean Removes the existing repository folder from the local file system before extracting the archive. This ensures that local repository matches the source GitHub repository. Note: this should only be used with PowerShell module/DSC repositories. #> [CmdletBinding(DefaultParameterSetName = 'Clean')] [OutputType([System.IO.DirectoryInfo])] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Owner, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master', [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath = "$env:ProgramFiles\WindowsPowershell\Modules", [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepository, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Force')] [System.Management.Automation.SwitchParameter] $Force, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Clean')] [System.Management.Automation.SwitchParameter] $Clean ) process { $uri = ResolveGitHubUri -Owner $Owner -Repository $Repository -Branch $Branch; $tempDestinationFilename = '{0}-{1}.zip' -f $Repository, $Branch; $tempDestinationPath = Join-Path -Path $env:TEMP -ChildPath $tempDestinationFilename; [ref] $null = Invoke-WebRequest -Uri $uri.AbsoluteUri -OutFile $tempDestinationPath; Unblock-File -Path $tempDestinationPath; $expandZipArchiveParams = @{ Path = $tempDestinationPath; DestinationPath = $DestinationPath; Repository = $Repository; Branch = $Branch; Force = $Force; Clean = $Clean; } if ($OverrideRepository) { $expandZipArchiveParams['OverrideRepository'] = $OverrideRepository; } [ref] $null = ExpandZipArchive @expandZipArchiveParams; $modulePath = Join-Path -Path $DestinationPath -ChildPath $Repository; if ($OverrideRepository) { $modulePath = Join-Path -Path $DestinationPath -ChildPath $OverrideRepository; } return (Get-Item -Path $modulePath); } #end process } #end function Install-GitHubRepository |