Skip to content

Instantly share code, notes, and snippets.

@milnak
Created September 24, 2025 23:25
Show Gist options
  • Save milnak/7b862349db234c942f19af98dac51bcd to your computer and use it in GitHub Desktop.
Save milnak/7b862349db234c942f19af98dac51bcd to your computer and use it in GitHub Desktop.
Recursive github repo downloader
# Create a new API token by going to
# https://github.com/settings/tokens/new (provide a description and check "repo" scope")
param (
# github repo path to download, e.g.
# 'https://github.com/microsoft/CsWinRT/tree/master/src/Samples/NetProjectionSample'
[Uri]$RepoPath = 'https://github.com/microsoft/CsWinRT/tree/master/src/Samples/NetProjectionSample',
# Folder to download to.
[Parameter(Mandatory)][string]$Path,
# GitHub API token for "repo" scope.
[string]$AccessToken
)
function Invoke-GitHubWebRequest {
[CmdletBinding()]
param(
[Parameter(Mandatory)][Uri]$Uri,
[string]$OutFile
)
Write-Verbose "Downloading $Uri"
$prevProgressPreference = $global:ProgressPreference
$originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol
$global:ProgressPreference = 'SilentlyContinue'
try {
# Enforce TLS v1.2 Security Protocol
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$headers = @{
'User-Agent' = 'GitDown for PowerShell'
'X-GitHub-Api-Version' = '2022-11-28'
}
if ($AccessToken) {
$headers += @{'Authorization' = "token $AccessToken" }
}
$invokeWebRequestParms = @{
'Uri' = $Uri
'Method' = 'GET'
'Headers' = $headers
'UseDefaultCredentials' = $true
'UseBasicParsing' = $true
'TimeoutSec' = 60
'Verbose' = $false
}
if ($OutFile) {
$invokeWebRequestParms += @{ 'OutFile' = $OutFile }
}
Invoke-WebRequest @invokeWebRequestParms
}
finally {
[Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol
$global:ProgressPreference = $prevProgressPreference
}
}
function Invoke-RepoWalk {
[CmdletBinding()]
param (
[Parameter(Mandatory)][Uri]$Uri,
[string]$AccessToken
)
Write-Verbose "Walking $Uri"
$response = Invoke-GitHubWebRequest -Uri $Uri
$content = $response.Content | ConvertFrom-Json
foreach ($obj in $content) {
if ($obj.type -eq 'file') {
[PSCustomObject]@{
Name = $obj.name # e.g. 'README.md'
RelativePath = (Split-Path -Parent $obj.path) # e.g. 'src\Samples\NetProjectionSample'
Size = $obj.size # File size
Sha1 = $obj.Sha # Is this SHA1?!?
Url = $obj.download_url # full download url
}
}
elseif ($obj.type -eq 'dir') {
Invoke-RepoWalk -Uri $obj.url -AccessToken $AccessToken
}
else {
throw "Unknown type $($obj.type)"
}
}
}
# e.g. 'https://github.com/microsoft/CsWinRT/tree/master/src/Samples/NetProjectionSample'
$info = @{
Author = $RepoPath.Segments[1] -replace '/', '' # e.g. 'microsoft'
Repository = $RepoPath.Segments[2] -replace '/', '' # e.g. 'CsWinRT'
Branch = $RepoPath.Segments[4] -replace '/', '' # e.g. 'master'
}
# e.g. 'src/Samples/NetProjectionSample'
$resourcePath = $RepoPath.Segments[-3..-1] -join ''
# e.g. https://api.github.com/repos/microsoft/CsWinRT/contents/src/Samples/NetProjectionSample?ref=master
$apiUri = 'https://api.github.com/repos/{0}/{1}/contents/{2}?ref={3}' -f $info['author'], $info['repository'], $resourcePath, $info['branch']
$downloadList = Invoke-RepoWalk -Uri $apiUri -AccessToken $AccessToken
foreach ($obj in $downloadList) {
$targetFolder = Join-Path $Path $obj.RelativePath
if (-not (Test-Path $targetFolder)) {
mkdir $targetFolder | Out-Null
}
$targetFile = Join-Path $targetFolder $obj.Name
$response = Invoke-GitHubWebRequest -Uri $obj.Url -OutFile $targetFile
$item = Get-Item -LiteralPath $targetFile
if ($item.Length -ne $obj.Size) {
throw "Incorrect size for $targetFile : $($item.Length); expected: $($obj.Size)"
}
$hash = (Get-FileHash -LiteralPath $targetFile -Algorithm SHA1).Hash
if ($hash -ne $obj.Sha1) {
# throw "Incorrect hash for $targetFile : $hash; expected: $($obj.Sha1)"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment