Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Download Git LFS file from TFS/VSTS
<#
.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
You can’t perform that action at this time.