| + |
use std::collections::{BTreeMap, BTreeSet};
|
| + |
use std::str::FromStr;
|
| + |
|
| + |
use cid::Cid;
|
| + |
use futures_lite::StreamExt;
|
| + |
use iroh::Endpoint;
|
| + |
use iroh_blobs::api::downloader::{DownloadProgressItem, Downloader};
|
| + |
use iroh_blobs::api::Store;
|
| + |
use iroh_blobs::{BlobFormat, HashAndFormat};
|
| + |
use radicle::identity::Did;
|
| + |
use radicle_artifact::share::{cid_utils, keys};
|
| + |
use url::Url;
|
| + |
|
| + |
use crate::error::Error;
|
| + |
|
| + |
/// Progress stages reported during an in-flight fetch. The frontend uses
|
| + |
/// these to render per-CID download status.
|
| + |
pub enum FetchStage {
|
| + |
Connecting,
|
| + |
Downloading { bytes: u64 },
|
| + |
}
|
| + |
|
| + |
/// Fetch an artifact (blob or collection) from any of the iroh providers
|
| + |
/// reachable through `locations`. Streams `on_progress` events as the
|
| + |
/// download progresses. Bytes land in `store` and are verified against the
|
| + |
/// CID via iroh-blobs' built-in hash check.
|
| + |
pub async fn fetch_artifact<F>(
|
| + |
store: &Store,
|
| + |
endpoint: &Endpoint,
|
| + |
cid: &Cid,
|
| + |
locations: &BTreeMap<Did, BTreeSet<Url>>,
|
| + |
mut on_progress: F,
|
| + |
) -> Result<(), Error>
|
| + |
where
|
| + |
F: FnMut(FetchStage),
|
| + |
{
|
| + |
let kind = cid_utils::artifact_kind(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
|
| + |
let hash = cid_utils::cid_to_blake3_hash(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
|
| + |
let format = match kind {
|
| + |
cid_utils::ArtifactKind::Blob => BlobFormat::Raw,
|
| + |
cid_utils::ArtifactKind::Collection => BlobFormat::HashSeq,
|
| + |
};
|
| + |
|
| + |
let providers = resolve_iroh_providers(locations);
|
| + |
if providers.is_empty() {
|
| + |
return Err(Error::Iroh(format!(
|
| + |
"no iroh providers reachable for {cid}"
|
| + |
)));
|
| + |
}
|
| + |
|
| + |
let downloader = Downloader::new(store, endpoint);
|
| + |
let mut stream = downloader
|
| + |
.download(HashAndFormat { hash, format }, providers)
|
| + |
.stream()
|
| + |
.await
|
| + |
.map_err(|e| Error::Iroh(format!("download init: {e}")))?;
|
| + |
|
| + |
while let Some(item) = stream.next().await {
|
| + |
match item {
|
| + |
DownloadProgressItem::TryProvider { .. } => on_progress(FetchStage::Connecting),
|
| + |
DownloadProgressItem::Progress(bytes) => {
|
| + |
on_progress(FetchStage::Downloading { bytes })
|
| + |
}
|
| + |
DownloadProgressItem::PartComplete { .. } | DownloadProgressItem::ProviderFailed { .. } => {}
|
| + |
DownloadProgressItem::Error(e) => return Err(Error::Iroh(format!("download: {e}"))),
|
| + |
DownloadProgressItem::DownloadError => {
|
| + |
return Err(Error::Iroh("download failed".into()));
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
Ok(())
|
| + |
}
|
| + |
|
| + |
/// Walk every (did, url) pair on an artifact and recover an iroh provider
|
| + |
/// public key. Two URL conventions are supported:
|
| + |
///
|
| + |
/// - `iroh://` (no host) — derive provider key from the contributor DID
|
| + |
/// via `did_to_iroh_public_key`. radworks-app and any peer that reuses
|
| + |
/// the Radicle keystore for iroh write locations in this form.
|
| + |
/// - `iroh://{endpoint_id_z32}` — parse the URL host with
|
| + |
/// `iroh::PublicKey::from_str`. radicle-desktop writes locations in
|
| + |
/// this form because its iroh key is decoupled from the Radicle key.
|
| + |
///
|
| + |
/// HTTP/HTTPS URLs are skipped here; they route through a separate
|
| + |
/// fallback path (see `radicle_artifact::share::download_http`).
|
| + |
fn resolve_iroh_providers(locations: &BTreeMap<Did, BTreeSet<Url>>) -> Vec<iroh::PublicKey> {
|
| + |
let mut out = Vec::new();
|
| + |
for (did, urls) in locations {
|
| + |
for url in urls {
|
| + |
if url.scheme() != "iroh" {
|
| + |
continue;
|
| + |
}
|
| + |
let key = match url.host_str() {
|
| + |
None | Some("") => keys::did_to_iroh_public_key(did).ok(),
|
| + |
Some(host) => iroh::PublicKey::from_str(host).ok(),
|
| + |
};
|
| + |
if let Some(k) = key {
|
| + |
out.push(k);
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
out
|
| + |
}
|