Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- New `artifacts_download` higher-level module for downloading Universal Packages from Azure DevOps Artifacts.
- Add example for downloading universal packages (`artifacts_download`).

### Changes

- Update `azure_core` and `azure_identity` to 0.34.
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,21 @@ This repo contains:

- [autorust](autorust/): A tool to autogenerate the `azure_devops_rust_api` crate from the OpenAPI spec.
- [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.
- [azure_devops_rust_api](azure_devops_rust_api/): The autogenerated crate.
- [azure_devops_rust_api](azure_devops_rust_api/): The autogenerated crate, plus the hand-written `artifacts_download` module (see below).

## Artifact downloads

Most of `azure_devops_rust_api` is auto-generated from the OpenAPI spec and provides thin wrappers
around the Azure DevOps REST API endpoints. The `artifacts_download` module is different: it is a
hand-written, higher-level module that implements the full protocol for downloading
[Universal Packages](https://docs.microsoft.com/en-us/azure/devops/artifacts/universal-packages/universal-packages-overview)
from Azure Artifacts.

It handles the entire download flow — service URL discovery, package metadata retrieval, blob URL
resolution, chunk download, decompression, and file reassembly — behind a single
`download_universal_package` call.

See [azure_devops_rust_api/README.md](azure_devops_rust_api/README.md) for usage details.

## Usage of generated `azure_devops_rust_api` crate

Expand Down
5 changes: 5 additions & 0 deletions azure_devops_rust_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ no-default-tag = []
accounts = []
approvals_and_checks = []
artifacts = []
artifacts_download = []
artifacts_package_types = []
audit = []
build = []
Expand Down Expand Up @@ -299,3 +300,7 @@ required-features = ["release"]
[[example]]
name = "member_entitlement_management"
required-features = ["member_entitlement_management"]

[[example]]
name = "download_artifact"
required-features = ["artifacts_download"]
47 changes: 47 additions & 0 deletions azure_devops_rust_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,53 @@ Example:
cargo run --example git_repo_get --features="git" <repo-name>
```

## Artifact downloads

In addition to the auto-generated REST API wrappers, the crate includes a higher-level
`artifacts_download` module for downloading [Universal Packages](https://docs.microsoft.com/en-us/azure/devops/artifacts/universal-packages/universal-packages-overview)
from Azure Artifacts.

Unlike the other modules, `artifacts_download` is not a thin wrapper around a single REST
endpoint. It implements the full dedup-based download protocol used by Azure Artifacts:
discovering service URLs, fetching package metadata, resolving blob IDs, downloading and
decompressing content chunks, and reassembling them into files on disk.

Enable it with the `artifacts_download` feature:

```toml
[dependencies]
azure_devops_rust_api = { version = "0.35.0", features = ["artifacts_download"] }
```

Example usage (from [examples/download_artifact.rs](examples/download_artifact.rs)):

```rust
let client = artifacts_download::ClientBuilder::new(credential).build();

let metadata = client
.download_universal_package(
&organization,
&project,
&feed,
&package_name,
&version,
&output_path,
)
.await?;

println!(
"Downloaded {} v{} ({} bytes) to {:?}",
package_name, metadata.version, metadata.package_size, output_path
);
```

Run the example:

```sh
cargo run --example download_artifact --features="artifacts_download" -- \
--feed <feed> --name <package-name> --version <version> --path <output-dir>
```

## Issue reporting

If you find any issues then please raise them via [Github](https://github.com/microsoft/azure-devops-rust-api/issues).
Expand Down
108 changes: 108 additions & 0 deletions azure_devops_rust_api/examples/download_artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Download a Universal Package from Azure Artifacts.
//
// Usage:
// export ADO_ORGANIZATION=<org>
// export ADO_PROJECT=<project>
// cargo run --example download_artifact --features="artifacts_download" -- \
// --feed <feed> --name <package-name> --version <version> --path <output-dir>

use anyhow::{Context, Result};
use azure_devops_rust_api::artifacts_download;
use std::env;
use std::path::PathBuf;

mod utils;

// --- CLI argument parsing ---

struct Args {
organization: String,
project: String,
feed: String,
name: String,
version: String,
path: PathBuf,
}

fn parse_args() -> Result<Args> {
let organization = env::var("ADO_ORGANIZATION").context("Must define ADO_ORGANIZATION")?;
let project = env::var("ADO_PROJECT").context("Must define ADO_PROJECT")?;

let args: Vec<String> = env::args().collect();
let mut feed = None;
let mut name = None;
let mut version = None;
let mut path = None;

let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--feed" => {
feed = Some(args.get(i + 1).context("--feed requires a value")?.clone());
i += 2;
}
"--name" => {
name = Some(args.get(i + 1).context("--name requires a value")?.clone());
i += 2;
}
"--version" => {
version = Some(
args.get(i + 1)
.context("--version requires a value")?
.clone(),
);
i += 2;
}
"--path" => {
path = Some(args.get(i + 1).context("--path requires a value")?.clone());
i += 2;
}
_ => {
i += 1;
}
}
}

Ok(Args {
organization,
project,
feed: feed.context("--feed is required")?,
name: name.context("--name is required")?,
version: version.context("--version is required")?,
path: PathBuf::from(path.context("--path is required")?),
})
}

#[tokio::main]
async fn main() -> Result<()> {
let args = parse_args()?;
let credential = utils::get_credential()?;

println!(
"Downloading Universal Package: {}@{} from {}/{}",
args.name, args.version, args.organization, args.project
);

let client = artifacts_download::ClientBuilder::new(credential).build();

let metadata = client
.download_universal_package(
&args.organization,
&args.project,
&args.feed,
&args.name,
&args.version,
&args.path,
)
.await?;

println!(
"Downloaded {} v{} ({} bytes) to {:?}",
args.name, metadata.version, metadata.package_size, args.path
);

Ok(())
}
Loading
Loading