Skip to content

Commit eaef4a4

Browse files
cataggarCopilotjohnbattyclaude
authored
Add artifacts_download module and example for universal packages (#763)
* Add example for downloading universal packages Addresses #330: Adds a complete example demonstrating how to download universal packages from Azure DevOps Artifacts, plus an artifacts_download module with the reusable protocol implementation. The download protocol involves: 1. Service discovery via ResourceAreas API 2. Package metadata retrieval from the packages service 3. Blob URL resolution via the dedup service 4. Manifest download and parsing 5. Chunk download with xpress decompression * Apply cargo fmt to artifacts_download module Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cargo fmt --all Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor text collection in collect_text function * Update artifacts_download to azure_core 0.33 API The azure_core crate restructured its public API. Update the hand-written artifacts_download module to use the new paths and renamed functions. Key changes: - Move types under azure_core::http (Pipeline, ClientOptions, RetryOptions, Transport, Context, RawResponse, Method, Request, Url, headers) - Replace removed EMPTY_BODY with Bytes::new() - Replace azure_core::to_json with azure_core::json::to_json - Rename Error::message -> Error::with_message, Error::full -> Error::with_error - Rename ResultExt::context -> ResultExt::with_context - Pipeline::new and Pipeline::send take an extra None options argument - Response body is now sync (RawResponse::into_body() returns ResponseBody, no .collect().await needed) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix clippy warnings: replace cloned slice refs with slice::from_ref Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Document artifacts_download higher-level module in READMEs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Apply cargo fmt to artifacts_download/mod.rs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix println format in artifacts_download README example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add artifacts_download entries to CHANGELOG Unreleased section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: John Batty <john@glowingslab.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c01c588 commit eaef4a4

8 files changed

Lines changed: 1119 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- New `artifacts_download` higher-level module for downloading Universal Packages from Azure DevOps Artifacts.
13+
- Add example for downloading universal packages (`artifacts_download`).
14+
1015
### Changes
1116

1217
- Update `azure_core` and `azure_identity` to 0.34.

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@ This repo contains:
1212

1313
- [autorust](autorust/): A tool to autogenerate the `azure_devops_rust_api` crate from the OpenAPI spec.
1414
- [vsts-api-patcher](vsts-api-patcher/): A tool to patch the OpenAPI spec. This modifies the original OpenAPI spec to fix known issues and/or improve the generated code.
15-
- [azure_devops_rust_api](azure_devops_rust_api/): The autogenerated crate.
15+
- [azure_devops_rust_api](azure_devops_rust_api/): The autogenerated crate, plus the hand-written `artifacts_download` module (see below).
16+
17+
## Artifact downloads
18+
19+
Most of `azure_devops_rust_api` is auto-generated from the OpenAPI spec and provides thin wrappers
20+
around the Azure DevOps REST API endpoints. The `artifacts_download` module is different: it is a
21+
hand-written, higher-level module that implements the full protocol for downloading
22+
[Universal Packages](https://docs.microsoft.com/en-us/azure/devops/artifacts/universal-packages/universal-packages-overview)
23+
from Azure Artifacts.
24+
25+
It handles the entire download flow — service URL discovery, package metadata retrieval, blob URL
26+
resolution, chunk download, decompression, and file reassembly — behind a single
27+
`download_universal_package` call.
28+
29+
See [azure_devops_rust_api/README.md](azure_devops_rust_api/README.md) for usage details.
1630

1731
## Usage of generated `azure_devops_rust_api` crate
1832

azure_devops_rust_api/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ no-default-tag = []
5252
accounts = []
5353
approvals_and_checks = []
5454
artifacts = []
55+
artifacts_download = []
5556
artifacts_package_types = []
5657
audit = []
5758
build = []
@@ -299,3 +300,7 @@ required-features = ["release"]
299300
[[example]]
300301
name = "member_entitlement_management"
301302
required-features = ["member_entitlement_management"]
303+
304+
[[example]]
305+
name = "download_artifact"
306+
required-features = ["artifacts_download"]

azure_devops_rust_api/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,53 @@ Example:
100100
cargo run --example git_repo_get --features="git" <repo-name>
101101
```
102102

103+
## Artifact downloads
104+
105+
In addition to the auto-generated REST API wrappers, the crate includes a higher-level
106+
`artifacts_download` module for downloading [Universal Packages](https://docs.microsoft.com/en-us/azure/devops/artifacts/universal-packages/universal-packages-overview)
107+
from Azure Artifacts.
108+
109+
Unlike the other modules, `artifacts_download` is not a thin wrapper around a single REST
110+
endpoint. It implements the full dedup-based download protocol used by Azure Artifacts:
111+
discovering service URLs, fetching package metadata, resolving blob IDs, downloading and
112+
decompressing content chunks, and reassembling them into files on disk.
113+
114+
Enable it with the `artifacts_download` feature:
115+
116+
```toml
117+
[dependencies]
118+
azure_devops_rust_api = { version = "0.35.0", features = ["artifacts_download"] }
119+
```
120+
121+
Example usage (from [examples/download_artifact.rs](examples/download_artifact.rs)):
122+
123+
```rust
124+
let client = artifacts_download::ClientBuilder::new(credential).build();
125+
126+
let metadata = client
127+
.download_universal_package(
128+
&organization,
129+
&project,
130+
&feed,
131+
&package_name,
132+
&version,
133+
&output_path,
134+
)
135+
.await?;
136+
137+
println!(
138+
"Downloaded {} v{} ({} bytes) to {:?}",
139+
package_name, metadata.version, metadata.package_size, output_path
140+
);
141+
```
142+
143+
Run the example:
144+
145+
```sh
146+
cargo run --example download_artifact --features="artifacts_download" -- \
147+
--feed <feed> --name <package-name> --version <version> --path <output-dir>
148+
```
149+
103150
## Issue reporting
104151

105152
If you find any issues then please raise them via [Github](https://github.com/microsoft/azure-devops-rust-api/issues).
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Download a Universal Package from Azure Artifacts.
5+
//
6+
// Usage:
7+
// export ADO_ORGANIZATION=<org>
8+
// export ADO_PROJECT=<project>
9+
// cargo run --example download_artifact --features="artifacts_download" -- \
10+
// --feed <feed> --name <package-name> --version <version> --path <output-dir>
11+
12+
use anyhow::{Context, Result};
13+
use azure_devops_rust_api::artifacts_download;
14+
use std::env;
15+
use std::path::PathBuf;
16+
17+
mod utils;
18+
19+
// --- CLI argument parsing ---
20+
21+
struct Args {
22+
organization: String,
23+
project: String,
24+
feed: String,
25+
name: String,
26+
version: String,
27+
path: PathBuf,
28+
}
29+
30+
fn parse_args() -> Result<Args> {
31+
let organization = env::var("ADO_ORGANIZATION").context("Must define ADO_ORGANIZATION")?;
32+
let project = env::var("ADO_PROJECT").context("Must define ADO_PROJECT")?;
33+
34+
let args: Vec<String> = env::args().collect();
35+
let mut feed = None;
36+
let mut name = None;
37+
let mut version = None;
38+
let mut path = None;
39+
40+
let mut i = 1;
41+
while i < args.len() {
42+
match args[i].as_str() {
43+
"--feed" => {
44+
feed = Some(args.get(i + 1).context("--feed requires a value")?.clone());
45+
i += 2;
46+
}
47+
"--name" => {
48+
name = Some(args.get(i + 1).context("--name requires a value")?.clone());
49+
i += 2;
50+
}
51+
"--version" => {
52+
version = Some(
53+
args.get(i + 1)
54+
.context("--version requires a value")?
55+
.clone(),
56+
);
57+
i += 2;
58+
}
59+
"--path" => {
60+
path = Some(args.get(i + 1).context("--path requires a value")?.clone());
61+
i += 2;
62+
}
63+
_ => {
64+
i += 1;
65+
}
66+
}
67+
}
68+
69+
Ok(Args {
70+
organization,
71+
project,
72+
feed: feed.context("--feed is required")?,
73+
name: name.context("--name is required")?,
74+
version: version.context("--version is required")?,
75+
path: PathBuf::from(path.context("--path is required")?),
76+
})
77+
}
78+
79+
#[tokio::main]
80+
async fn main() -> Result<()> {
81+
let args = parse_args()?;
82+
let credential = utils::get_credential()?;
83+
84+
println!(
85+
"Downloading Universal Package: {}@{} from {}/{}",
86+
args.name, args.version, args.organization, args.project
87+
);
88+
89+
let client = artifacts_download::ClientBuilder::new(credential).build();
90+
91+
let metadata = client
92+
.download_universal_package(
93+
&args.organization,
94+
&args.project,
95+
&args.feed,
96+
&args.name,
97+
&args.version,
98+
&args.path,
99+
)
100+
.await?;
101+
102+
println!(
103+
"Downloaded {} v{} ({} bytes) to {:?}",
104+
args.name, metadata.version, metadata.package_size, args.path
105+
);
106+
107+
Ok(())
108+
}

0 commit comments

Comments
 (0)