Backup (NuGet) Packages for All Azure DevOps Organization Feeds with PowerShell
This quick blog post demonstrates how to automate the backup of all packages from all organization feeds on Azure DevOps using a scheduled pipeline and a PowerShell script. The script leverages the Azure DevOps REST API to enumerate feeds and download every package version to a network share.
It handles skipping already downloaded packages and for our case can handle checking several feeds and ~10,000 packages and versions in less than one minute when no new packages found. No further details are provided here, but the script is available below, I thought perhaps someone might find it useful as a reference and the code is pretty straighforward.
Note that this only works for organization feeds not project feeds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
trigger: none
schedules:
# 01:00 UTC every Sunday
- cron: "0 1 * * 0"
displayName: Weekly NuGet Backup
branches:
include:
- main
always: true
pool:
name: 'Default'
variables:
ORGANIZATION_NAME: 'YOURORGNAME'
NETWORK_PATH: '\\YOURLOCALPATH' # UNC path to your network share
steps:
- task: PowerShell@2
displayName: Download NuGet packages from all feeds
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
targetType: inline
script: |
$ErrorActionPreference = 'Stop'
$org = "$(ORGANIZATION_NAME)"
$networkBase = "$(NETWORK_PATH)"
$token = $env:SYSTEM_ACCESSTOKEN
$headers = @{ Authorization = "Bearer $token" }
# Get all feeds
$feedsUrl = "https://feeds.dev.azure.com/$org/_apis/packaging/feeds?api-version=7.1-preview.1"
Write-Host "Fetching all feeds from $feedsUrl"
$feedsResp = Invoke-RestMethod -Uri $feedsUrl -Headers $headers
$feeds = $feedsResp.value
Write-Host "Feeds count $($feeds.Count)"
if ($feeds.Count -eq 0) {
Write-Host "No feeds found."
exit 0
}
foreach ($feed in $feeds) {
$feedName = $feed.name
$feedId = $feed.id
$feedPath = Join-Path $networkBase $feedName
Write-Host "=================================================================="
Write-Host "====== '$($feed.name)'"
Write-Host "=================================================================="
$fetchBaseUrl = "https://feeds.dev.azure.com/$org/_apis/packaging/feeds/$feedId/packages?protocolType=NuGet&api-version=7.1"
$downloadBaseUrl = "https://pkgs.dev.azure.com/$org/_apis/packaging/feeds/$feedId/nuget/packages/"
$pageSize = 50000
$skip = 0
$allPkgs = @()
$page = 1
# Fetch package names and versions from the feed
Write-Host "Fetching packages from feed '$feedName'..."
while ($true) {
$url = "$fetchBaseUrl&includeAllVersions=true&`$top=$pageSize&`$skip=$skip"
Write-Host "Requesting page $($page) from $($url)"
$resp = Invoke-RestMethod -Uri $url -Headers $headers
if ($resp.value.Count -eq 0) {
break
}
$allPkgs += $resp.value
Write-Host "Fetched $($resp.value.Count) packages in page $($page)."
$skip += $resp.value.Count
$page++
}
Write-Host "Package names found $($allPkgs.Count) in feed '$feedName'"
if ($allPkgs.Count -eq 0) {
Write-Host "WARNING No packages found in feed '$feedName'."
continue
}
# Ensure the target directory exists
if (-not (Test-Path $feedPath)) {
Write-Host "Creating directory '$feedPath'..."
New-Item -ItemType Directory -Path $feedPath -Force | Out-Null
} else {
Write-Host "Directory '$feedPath' already exists."
}
# Download packages (skip already exists)
$totalPackagesCount = 0
foreach ($pkg in $allPkgs) {
Write-Host "====== '$($pkg.name)' versions found $($pkg.versions.Count) ======"
foreach ($ver in $pkg.versions) {
$nupkgFileName = "$($pkg.name).$($ver.version).nupkg"
$nupkgPath = Join-Path $feedPath $nupkgFileName
if (Test-Path $nupkgPath) {
Write-Host "Skipping '$($nupkgPath)' (already exists)"
continue
}
$downloadUrl = "$($downloadBaseUrl)$($pkg.name)/versions/$($ver.version)/content?api-version=7.1-preview"
Write-Host "Downloading '$($nupkgFileName)' to '$feedPath' from '$($downloadUrl)'"
Invoke-RestMethod -Uri $downloadUrl -Headers $headers -OutFile $nupkgPath
}
$totalPackagesCount += $pkg.versions.Count
}
Write-Host "Total packages found (already downloaded or downloaded) $($totalPackagesCount) in feed '$feedName'"
}
That’s all!