use anyhow::{Context, Result}; use serde::Deserialize; use std::path::{Path, PathBuf}; #[derive(Debug, Deserialize)] struct Release { assets: Vec, } #[derive(Debug, Deserialize)] struct Asset { name: String, browser_download_url: String, } /// Downloads the release asset named exactly `asset_name` for `repo`@`tag` /// into `dest_dir`, returning the local path. pub fn download_asset( client: &reqwest::blocking::Client, repo: &str, tag: &str, asset_name: &str, dest_dir: &Path, ) -> Result { let api_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}"); let release: Release = client .get(&api_url) .send()? .error_for_status() .with_context(|| format!("fetching release metadata from {api_url}"))? .json()?; let asset = release .assets .iter() .find(|a| a.name == asset_name) .with_context(|| format!("no asset named '{asset_name}' in {repo}@{tag}"))?; std::fs::create_dir_all(dest_dir)?; let dest_path = dest_dir.join(&asset.name); let bytes = client .get(&asset.browser_download_url) .send()? .error_for_status()? .bytes()?; std::fs::write(&dest_path, &bytes)?; Ok(dest_path) }