Stream a release asset to disk¶
Pick the asset¶
A Release carries a Vec<ReleaseAsset>. Nothing selects one for you —
matching a filename to the running platform is your tool's decision, because
only your tool knows its naming convention.
let release = provider.latest_release().await?;
let want = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
let asset = release
.assets
.iter()
.find(|a| a.name.contains(&want))
.ok_or_else(|| anyhow::anyhow!(
"no asset matching `{want}` in release {}; available: {:?}",
release.tag,
release.assets.iter().map(|a| &a.name).collect::<Vec<_>>()
))?;
ReleaseAsset::name is the filename, not a URL. download_url is the
fully-qualified URL and is the field download_asset uses.
Stream it¶
download_asset returns an AsyncRead and the content length the host
reported. Copy it straight into the destination:
use tokio::io::AsyncWriteExt as _;
let (mut reader, reported_len) = provider.download_asset(asset).await?;
let mut file = tokio::fs::File::create("/tmp/download.partial").await?;
let written = tokio::io::copy(&mut reader, &mut file).await?;
file.flush().await?;
The body is streamed, not buffered — a 200 MB asset does not become a 200 MB
Vec<u8>.
Do not trust the reported length¶
reported_len is the Content-Length header, or 0 when the header is
absent. Chunked responses report 0, and even when present the value is the
server's claim.
Use it to drive a progress bar and to sanity-check afterwards. Do not use it to
size an allocation, and do not treat 0 as "empty":
if reported_len != 0 && written != reported_len {
anyhow::bail!("truncated download: expected {reported_len} bytes, got {written}");
}
Enforce your own ceiling if the asset comes from anywhere you do not control —
tokio::io::copy will happily write until the connection ends.
Download to a temporary path, then rename¶
download_asset has no resume and no integrity check. A connection dropped
halfway leaves a partial file. Write to a scratch path and rename only after
the copy returns:
If the release publishes checksums as a separate asset, fetch and verify that before the rename. Nothing in this crate does it for you.
Handle the failures that occur¶
use rtb_forge::ProviderError;
match provider.download_asset(asset).await {
Err(ProviderError::NotFound { what }) => {
// The asset URL 404'd. Common when the release metadata is
// cached and the asset was replaced, or on a direct source
// where the tag was never verified.
}
Err(ProviderError::Unauthorized { host }) => {
// 401. The token is missing, expired, or wrong for `host`.
}
Err(ProviderError::RateLimited { host, retry_after }) => {
// Back off. `retry_after` is None when the host did not say.
let wait = retry_after.unwrap_or(std::time::Duration::from_secs(60));
}
Err(ProviderError::Transport(msg)) => {
// Network failure, timeout — or a status the mapping does not
// recognise, in which case `msg` starts "unexpected status".
// A plain 403 from an under-scoped token arrives here.
}
Err(other) => return Err(other.into()),
Ok((reader, len)) => { /* … */ }
}
The variant list is #[non_exhaustive], so keep the _ arm.
Authenticate the download¶
Whether the download carries credentials is decided when the provider is built,
not per call — the factory's Option<SecretString> is reused for every
request including asset downloads. The right header for the backend is applied
automatically: Authorization: Bearer for GitHub and Direct, PRIVATE-TOKEN
for GitLab, Authorization: token for Gitea and Codeberg, HTTP Basic for
Bitbucket.
Bitbucket needs username in config as well as a token. With one but not
the other the request is sent anonymously and a private repository answers
404.
Know what each backend can and cannot give you¶
- Bitbucket pairs a tag to repository Downloads by filename substring.
Only the first 100 downloads are considered, matching is not version-aware
(tag
v1.2matchestool-1.2.10.tar.gz), and a download with no self link gets an emptydownload_urlthat fails as a transport error. - GitLab asset
sizeis always0andcontent_typecarries GitLab'slink_type, not a MIME type. - Direct synthesises exactly one asset per release from the URL template,
with
size0. There is no asset list to search.
Full detail in Release-provider methods and backend behaviour.