Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add fetch helper using iroh-blobs Downloader
Daniel Norman committed 7 days ago
commit f434db11b6728f99ca305788e4478063da1a2e4e
parent 443ac67d49434a7e3d228bfce25600b7b91211d2
2 files changed +105 -0
added crates/radicle-types/src/fetch.rs
@@ -0,0 +1,104 @@
+
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
+
}
modified crates/radicle-types/src/lib.rs
@@ -13,6 +13,7 @@ pub mod config;
pub mod diff;
pub mod domain;
pub mod error;
+
pub mod fetch;
pub mod oid;
pub mod outbound;
pub mod repo;