Created
June 15, 2017 04:29
-
-
Save kamranayub/1c6bcb09d3858a86b736ef39593fc575 to your computer and use it in GitHub Desktop.
Download Git LFS file from TFS/VSTS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Downloads a blob item from TFS Git LFS | |
.DESCRIPTION | |
This script performs a 4-step process to download a LFS-hosted file from TFS. It fetches the item metadata | |
then downloads the LFS pointer. After it validates the pointer file, it preps a LFS batch download and finally | |
downloads the file to the specified OutFile. You can use this cmdlet in a pipeline as it passes a Get-Item call | |
at the end. | |
Use -Verbose or -Debug to see the URLs and pointer information the script is using to diagnose any issues. | |
.PARAMETER Instance | |
The TFS base URL | |
.PARAMETER Collection | |
The TFS team project collection. Default: "DefaultCollection" | |
.PARAMETER TeamProject | |
The name of the Team Project the repository is in | |
.PARAMETER Repository | |
The name of the Git repository | |
.PARAMETER ItemPath | |
The relative path to the item from the root of the repository | |
.PARAMETER ItemVersion | |
The version of the item (can be a branch, commit, or tag). Default: "master" | |
.PARAMETER ItemVersionType | |
The version type of the item (can be branch, commit, or tag). Default: "branch" | |
.PARAMETER OutFile | |
The filename to output to, defaults to a temp file | |
.PARAMETER Credential | |
An optional credential to use to connect to TFS. Defaults to passing UseDefaultCredentials. | |
.NOTES | |
See my blog post regarding downloading Git LFS items via TFS API: | |
https://kamranicus.com/posts/2017-06-14-downloading-git-lfs-files-from-tfs-vsts | |
.TODO | |
The pointer fetch is naive. Needs to be updated to stream only enough bytes to validate | |
the pointer contents and then it should close the connection. Careful executing this | |
against files you aren't sure are Git LFS hosted. | |
#> | |
Param( | |
[ValidateNotNullOrEmpty()] | |
[string]$Instance, | |
[ValidateNotNullOrEmpty()] | |
[string]$Collection = "DefaultCollection", | |
[ValidateNotNullOrEmpty()] | |
[string]$TeamProject, | |
[ValidateNotNullOrEmpty()] | |
[string]$Repository, | |
[ValidateNotNullOrEmpty()] | |
[string]$ItemPath, | |
[ValidateNotNullOrEmpty()] | |
[string]$ItemVersion = "master", | |
[ValidateSet("branch", "commit", "tag")] | |
[string]$ItemVersionType = "branch", | |
[ValidateNotNullOrEmpty()] | |
[string]$OutFile = [System.IO.Path]::GetTempFileName(), | |
[ValidateNotNull()] | |
[System.Management.Automation.Credential()] | |
[PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty | |
) | |
$ErrorActionPreference = "Stop" | |
# Default to using default creds | |
$Credentials = @{ UseDefaultCredentials = $true } | |
# Override if cred is specified | |
if ($Credential.UserName -ne $null) { | |
$Credentials = @{ Credential = $Credential } | |
} | |
# Get item's metadata from TFS | |
$ItemApiUrl = ("$Instance/$Collection/$TeamProject/_apis/git/repositories/$Repository/items?api-version=1.0" ` | |
+ "&scopePath=$ItemPath" ` | |
+ "&versionType=$ItemVersionType" ` | |
+ "&version=$ItemVersion") | |
$ItemMetadata = Invoke-RestMethod ` | |
-Uri $ItemApiUrl ` | |
-Headers @{Accept="application/json"} ` | |
@Credentials | |
Write-Debug "Item metadata: $($ItemMetadata | ConvertTo-Json)" | |
if ($ItemMetadata.value.gitObjectType -ne "blob") { | |
Write-Error "Item is not a blob object type" | |
} | |
# Get the Git LFS pointer | |
# Since TFS doesn't really indicate whether this is an LFS-hosted file | |
# we have to get its contents to check :( | |
# TODO: Only grab first 4 lines as a string | |
$LfsPointer = Invoke-RestMethod ` | |
-Uri "$Instance/$Collection/$TeamProject/_apis/git/repositories/$Repository/blobs/$($ItemMetadata.value.objectId)?api-version=1.0&`$format=text" ` | |
@Credentials | |
if ($LfsPointer -notmatch '(?m)^oid') { | |
Write-Error "Item is not a Git LFS pointer file" | |
} | |
Write-Debug "LFS pointer:" | |
Write-Debug $LfsPointer | |
# Pick out sha256 hash object id | |
$LfsOid = ($LfsPointer -match '(?m)^oid sha256:([a-z0-9]+)$' | % {$Matches[1]}) | |
# Pick out object size | |
$LfsSize = [int]($LfsPointer -match '(?m)^size ([0-9]+)$' | % {$Matches[1]}) | |
if (!$LfsOid -or !$LfsSize) { | |
$LfsPointer | |
Write-Error "Could not discover LFS item oid or size" | |
} | |
# Issue a Git LFS Batch request | |
# | |
# Technically as of TFS 2017 Update 2, you could simply just GET | |
# the object URL and skip this step. But according to the LFS spec, | |
# this is a required step and we should follow it. | |
$LfsBatchRequest = @{ | |
operation = "download"; | |
transfers = @("basic"); | |
objects = @( | |
@{ oid = $LfsOid; size = $LfsSize; } | |
) | |
} | |
$LfsBatchRequestJson = ConvertTo-Json -InputObject $LfsBatchRequest # Ensure PS doesn't flatten array of 1 | |
Write-Debug "LFS batch request" | |
Write-Debug $LfsBatchRequestJson | |
$LfsBatchResponse = Invoke-RestMethod ` | |
-Uri "$Instance/$Collection/$TeamProject/_git/$Repository.git/info/lfs/objects/batch" ` | |
-Method Post ` | |
-Body $LfsBatchRequestJson ` | |
-ContentType "application/vnd.git-lfs+json" ` | |
-Headers @{ Accept = "application/vnd.git-lfs+json"; "Content-Type" = "application/vnd.git-lfs+json" } ` | |
@Credentials | |
# Download the object | |
Invoke-RestMethod ` | |
-Uri $LfsBatchResponse.objects._links.download.href ` | |
-Headers @{ Accept = "application/vnd.git-lfs" } ` | |
-OutFile $OutFile ` | |
@Credentials | |
Get-Item $OutFile |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment