public/Save-AzureDevOpsRepoItem.ps1
Function Save-AzureDevOpsRepoItem { <# .SYNOPSIS Gets a file or folder from Azure DevOps. .DESCRIPTION Gets a file or folder incl subfolders from Azure DevOps and preserve changed data timestamp from Azure DevOps. .EXAMPLE Invoke-SqlCmdWithMessages -Query 'select 0' -ServerInstance 'localhost' -Database master -OutputFile C:\tmp\test2.txt .PARAMETER Organization The name of the Azure DevOps organization .PARAMETER Repository The name of the Azure DevOps repository containg the items to save locally. Must start with $/ and end with / .PARAMETER ProjectPath The path to a file or folder within the repotitory eg. /Folder/file.ext. Must not start with /. If the ProjectPath points to a folder, it will get all files and subfolders recursively. If the ProjectPath points to a folder and does not end with / and point it will get the entire folder. If it does not end with /, it will only save the content of that folder. .PARAMETER ChangeSet Optional parameter to retrieve a specific version of a file or folder. .PARAMETER AzureDevOpsAccessToken .PARAMETER OutputPath The local folder to save the Azure DevOps files in. .OUTPUTS The content of the output folder .LINK https://github.com/DennisWagner/SQLServerDevOpsTools .NOTES Written by (c) Dennis Wagner Kristensen, 2021 https://github.com/DennisWagner/SQLServerDevOpsTools This PowerShell script is released under the MIT license http://www.opensource.org/licenses/MIT #> <# håndter adgang uden access token håndter nyeste version, ikke i changeset #> Param ( [Parameter(Mandatory=$true)] [string] $Organization, [Parameter(Mandatory=$true)] [ValidateScript({$_.StartsWith('$/') -and $_.EndsWith('/')})] [string] $Repository, [Parameter(Mandatory=$true)] [ValidateScript({-not $_.StartsWith('/')})] [string] $ProjectPath, [Parameter(Mandatory=$false)] [string] $ChangeSet, [Parameter(Mandatory=$false)] [string] $AzureDevOpsAccessToken, [Parameter(Mandatory=$true)] [string] $OutputPath ) BEGIN { $ScopePath = "$($Repository)$($ProjectPath)" $FolderPath = @{label="FullPath"; expression={Join-Path -Path $OutputFolder -ChildPath ($_.path.Replace($ScopePath, '')).Replace('/', '\')}} if (-not (Test-Path -Path $OutputPath)) { Write-Verbose "Creating output folder: $OutputPath." New-Item -Path $OutputPath -ItemType Directory | Out-Null } $IncludeTopfolder = -not $ProjectPath.EndsWith('/') $OutputFolder = $OutputPath $AzureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($AzureDevOpsPAT)")) } $uri = "https://dev.azure.com/$($Organization)/_apis/tfvc/items?api-version=6.0&recursionLevel=full&versionDescriptor.versionType=changeset" $uri += "&version=$($ChangeSet)" $uri += "&scopePath=$ScopePath" Write-Verbose "URL: $uri" } PROCESS { # get a list of the files and folders in the project based on the changeset #$result = Invoke-RestMethod -Uri $uri -Method get -Headers $AzureDevOpsAuthenicationHeader $response = Invoke-WebRequest -Uri $uri -Method get -Headers $AzureDevOpsAuthenicationHeader If ($response.StatusCode -eq 203) { Throw "There was an error accessing Azure DevOps: Error: $($response.StatusCode) $($response.StatusDescription). Response from Azure DevOps: $($response.Content)" } elseif ($response.StatusCode -ne 200) { Throw "There was an unknown error accessing Azure DevOps at URL: $($uri): Error: $($response.StatusCode) $($response.StatusDescription)." } $result = $response.Content | ConvertFrom-Json If ($result.count -eq 0) { Throw "Unable to locate the specified project path: $ProjectPath in the repository: $Repository for the Azure DevOps organization: $Organization" } If ($IncludeTopfolder) { $firstitem = $result.value | Select-Object -First 1 If ($firstitem.IsFolder -and $firstitem.path -eq $ScopePath) { # the ProjectPath points to a folder, and that must be included in the download $topfolder = ($firstitem.path -split '/') | Select-Object -Last 1 $OutputFolder = Join-Path -path $OutputFolder -ChildPath $topfolder } } else { Write-Verbose "Only get the content of the project path, in case it points to a folder." } $RepoItems = $result.value | Select-Object version, changeDate, size, hashValue, encoding, path, url, isFolder, $FolderPath # Download all files and folders in the project foreach ($item in ($RepoItems | Sort-Object Path)) { If (($item.path.length -le $ScopePath.length) -and $item.IsFolder) { # skip the top level level - only get the content of the folder continue } If ($item.isFolder) { if (-not (Test-Path -Path $item.FullPath)) { New-Item -Path $item.FullPath -ItemType Directory | Out-Null } } else { $FileName = ($item.path -split '/') | Select-Object -Last 1 $FolderPath = $item.FullPath -replace $FileName, "" If ($result.count -eq 1) { # The projects path points to a single file, so replace the FullPath. FullPath only works for folders $item.FullPath = Join-Path -Path $OutputFolder -ChildPath $FileName } else { # make sure the target folder exists in case the report path only contains files If (-not (Test-Path -Path $FolderPath)) { New-Item -Path $FolderPath -ItemType Directory | Out-Null } } Try { Invoke-WebRequest -Uri "$($item.url)" -OutFile $item.FullPath -Headers $AzureDevOpsAuthenicationHeader } Catch { Throw "There was an unknown error accessing Azure DevOps at URL: $($item.url): Error: $($_)" } } } # Set changed data in reverse order to make sure lastwritetime on folders is not updated by windows, when setting # the timestamps on content in the folder foreach ($item in ($RepoItems | Sort-Object Path -Descending)) { If (($item.path.length -le $ScopePath.length) -and $item.IsFolder) { # skip the top level level - only get the content of the folder If (-not $IncludeTopfolder) { continue } } $file = Get-Item $item.FullPath $file.LastWriteTime = $item.changeDate } } END { #Write-Host "" #Write-Host "Display content in output folder:" Get-ChildItem -Path $OutputPath -Recurse | Select-Object Name,Directory,LastWriteTime | Format-Table } } |