From faa89276b72639f14029ef1a240574853911128d Mon Sep 17 00:00:00 2001 From: Enrico Fusto Date: Mon, 15 Jun 2026 15:23:50 +0200 Subject: [PATCH] feat(catalog)support blob rich info in protocol --- src/core/protocol/catalog.rs | 82 +++++++++++++++++++++++++++- src/daemon/server/handlers/blob.rs | 17 +----- src/daemon/server/handlers/remote.rs | 26 ++++++++- src/util.rs | 16 ++++++ 4 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src/core/protocol/catalog.rs b/src/core/protocol/catalog.rs index 8d88534..414f239 100644 --- a/src/core/protocol/catalog.rs +++ b/src/core/protocol/catalog.rs @@ -36,7 +36,9 @@ use iroh::{ protocol::{AcceptError, ProtocolHandler}, Endpoint, EndpointId, }; -use iroh_blobs::{store::fs::FsStore, BlobFormat, Hash}; +use iroh_blobs::{ + api::blobs::BlobStatus, format::collection::Collection, store::fs::FsStore, BlobFormat, Hash, +}; use iroh_rings::{Permission, Registry}; use crate::core::{GrantStore, Privilege, ShareTicket}; @@ -67,6 +69,10 @@ pub struct CatalogEntry { pub name: String, /// Transfer ticket that can be passed to the local node to download this blob. pub ticket: ShareTicket, + /// Number of files in the collection; `None` for raw blobs or when unavailable. + pub file_count: Option, + /// Total content size in bytes; `None` when the information is unavailable. + pub total_size: Option, } /// iroh [`ProtocolHandler`] for the catalog protocol. @@ -152,7 +158,45 @@ impl CatalogHandler { let ticket = ShareTicket::from_format(addr.clone(), info.hash, info.format, Some(name.clone())); let ticket_uri = ticket.to_uri()?; - write_entry(send, info.hash, info.format, &name, &ticket_uri).await?; + + let (file_count, total_size) = match info.format { + BlobFormat::Raw => { + let size = match self.store.blobs().status(info.hash).await { + Ok(BlobStatus::Complete { size }) => Some(size), + _ => None, + }; + (None, size) + } + BlobFormat::HashSeq => match Collection::load(info.hash, &*self.store).await { + Ok(collection) => { + let fc = collection.iter().count(); + let mut sum: u64 = 0; + let mut complete = true; + for (_, fhash) in collection.iter() { + match self.store.blobs().status(*fhash).await { + Ok(BlobStatus::Complete { size }) => sum += size, + _ => { + complete = false; + break; + } + } + } + (Some(fc), if complete { Some(sum) } else { None }) + } + Err(_) => (None, None), + }, + }; + + write_entry( + send, + info.hash, + info.format, + &name, + &ticket_uri, + file_count, + total_size, + ) + .await?; } send.finish()?; @@ -219,11 +263,31 @@ pub(crate) async fn decode_entries( let ticket_uri = String::from_utf8(ticket_bytes).context("entry ticket is not UTF-8")?; let ticket = ShareTicket::from_uri(&ticket_uri)?; + let mut size_buf = [0u8; 8]; + recv.read_exact(&mut size_buf) + .await + .context("reading total_size")?; + let total_size = match u64::from_le_bytes(size_buf) { + u64::MAX => None, + v => Some(v), + }; + + let mut count_buf = [0u8; 4]; + recv.read_exact(&mut count_buf) + .await + .context("reading file_count")?; + let file_count = match u32::from_le_bytes(count_buf) { + u32::MAX => None, + v => Some(v as usize), + }; + entries.push(CatalogEntry { hash, format, name, ticket, + file_count, + total_size, }); } Ok(entries) @@ -235,6 +299,8 @@ async fn write_entry( format: BlobFormat, name: &str, ticket_uri: &str, + file_count: Option, + total_size: Option, ) -> Result<()> { send.write_all(hash.as_bytes()) .await @@ -252,6 +318,18 @@ async fn write_entry( write_length_prefixed(send, ticket_uri.as_bytes()) .await .context("writing ticket")?; + // u64::MAX sentinel = unknown size; u32::MAX sentinel = not applicable / unknown count. + send.write_all(&total_size.unwrap_or(u64::MAX).to_le_bytes()) + .await + .context("writing total_size")?; + send.write_all( + &file_count + .map(|c| c as u32) + .unwrap_or(u32::MAX) + .to_le_bytes(), + ) + .await + .context("writing file_count")?; Ok(()) } diff --git a/src/daemon/server/handlers/blob.rs b/src/daemon/server/handlers/blob.rs index 0f22069..abf187c 100644 --- a/src/daemon/server/handlers/blob.rs +++ b/src/daemon/server/handlers/blob.rs @@ -8,6 +8,8 @@ use uuid::Uuid; use crate::core::Node; use crate::daemon::protocol::Event; +use crate::util::format_size; + use super::{format_ring, resolve_target, send}; pub(crate) async fn handle_import( @@ -205,21 +207,6 @@ pub(crate) async fn handle_blob_list String { - const KIB: u64 = 1024; - const MIB: u64 = 1024 * KIB; - const GIB: u64 = 1024 * MIB; - if bytes >= GIB { - format!("{:.1} GiB", bytes as f64 / GIB as f64) - } else if bytes >= MIB { - format!("{:.1} MiB", bytes as f64 / MIB as f64) - } else if bytes >= KIB { - format!("{:.1} KiB", bytes as f64 / KIB as f64) - } else { - format!("{bytes} B") - } -} - pub(crate) async fn handle_blob_remove( req_id: Uuid, node: &Node, diff --git a/src/daemon/server/handlers/remote.rs b/src/daemon/server/handlers/remote.rs index 82fb5c4..0d17886 100644 --- a/src/daemon/server/handlers/remote.rs +++ b/src/daemon/server/handlers/remote.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::core::Node; use crate::daemon::protocol::Event; -use crate::util::parse_peer_id; +use crate::util::{format_size, parse_peer_id}; use super::send; @@ -37,12 +37,33 @@ pub(crate) async fn handle_remote_blob_list { + let count = entry + .file_count + .map(|n| format!("{n} files")) + .unwrap_or_else(|| "dir".into()); + format!("dir, {count}") + } + _ => "file".into(), + }; + let size_str = entry + .total_size + .map(format_size) + .unwrap_or_else(|| "?".into()); + send(tx, Event::blank(req_id)).await; send( tx, Event::line(req_id, format!(" {} ({})", entry.hash, entry.name)), ) .await; + send( + tx, + Event::line(req_id, format!(" kind: {kind_str} ({size_str})")), + ) + .await; send(tx, Event::line(req_id, format!(" ticket: {ticket_str}"))).await; send( tx, @@ -51,6 +72,9 @@ pub(crate) async fn handle_remote_blob_list String { format_peer_entry(peer, nick.as_deref()) } +/// Formats a byte count as a human-readable string (B / KiB / MiB / GiB). +pub(crate) fn format_size(bytes: u64) -> String { + const KIB: u64 = 1024; + const MIB: u64 = 1024 * KIB; + const GIB: u64 = 1024 * MIB; + if bytes >= GIB { + format!("{:.1} GiB", bytes as f64 / GIB as f64) + } else if bytes >= MIB { + format!("{:.1} MiB", bytes as f64 / MIB as f64) + } else if bytes >= KIB { + format!("{:.1} KiB", bytes as f64 / KIB as f64) + } else { + format!("{bytes} B") + } +} + /// Prints the ringdrop startup banner with version to stdout. /// /// The giraffe mascot is rendered in yellow on the left; the "ringdrop" text