diff --git a/Cargo.lock b/Cargo.lock index 6aae5dfb06..3f8abbd698 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3268,9 +3268,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.5.0" +version = "1.5.1" source = "registry+/~https://github.com/rust-lang/crates.io-index" -checksum = "b559898e0b4931ed2d3b959ab0c2da4d99cc644c4b0b1a35b4d344027f474023" +checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" [[package]] name = "positioned-io" diff --git a/iroh/src/commands.rs b/iroh/src/commands.rs index 79a28d25e7..641e40a8a1 100644 --- a/iroh/src/commands.rs +++ b/iroh/src/commands.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::{net::SocketAddr, path::PathBuf, time::Duration}; -use anyhow::Result; +use anyhow::{Context, Result}; use bytes::Bytes; use clap::{Args, Parser, Subcommand}; use colored::Colorize; @@ -567,6 +567,26 @@ pub enum BlobCommands { /// Delete content on the node. #[clap(subcommand)] Delete(self::delete::Commands), + /// Get a ticket to share this blob. + Share { + /// Hash of the blob to share. + hash: Hash, + /// Include an optional authentication token in the ticket. + #[clap(long)] + token: Option, + /// Do not include DERP reion information in the ticket. (advanced) + #[clap(long, conflicts_with = "derp_only", default_value_t = false)] + no_derp: bool, + /// Include only the DERP region information in the ticket. (advanced) + #[clap(long, conflicts_with = "no_derp", default_value_t = false)] + derp_only: bool, + /// If the blob is a collection, the requester will also fetch the listed blobs. + #[clap(long, default_value_t = false)] + recursive: bool, + /// Display the contents of this ticket too. + #[clap(long, hide = true)] + debug: bool, + }, } impl BlobCommands { @@ -638,6 +658,58 @@ impl BlobCommands { // node (last argument to run_with_opts). self::add::run_with_opts(iroh, opts, None).await } + Self::Share { + hash, + token, + no_derp, + derp_only, + recursive, + debug, + } => { + let NodeStatusResponse { addr, .. } = iroh.node.status().await?; + let node_addr = if no_derp { + PeerAddr::new(addr.peer_id) + .with_direct_addresses(addr.direct_addresses().copied()) + } else if derp_only { + if let Some(region) = addr.derp_region() { + PeerAddr::new(addr.peer_id).with_derp_region(region) + } else { + addr + } + } else { + addr + }; + + let blob_reader = iroh + .blobs + .read(hash) + .await + .context("failed to retrieve blob info")?; + let blob_status = if blob_reader.is_complete() { + "blob" + } else { + "incomplete blob" + }; + + let format = if recursive { + BlobFormat::HashSeq + } else { + BlobFormat::Raw + }; + + let request_token = token.map(RequestToken::new).transpose()?; + + let ticket = + Ticket::new(node_addr, hash, format, request_token).expect("correct ticket"); + println!( + "Ticket for {blob_status} {hash} ({})\n{ticket}", + HumanBytes(blob_reader.size()) + ); + if debug { + println!("{ticket:#?}") + } + Ok(()) + } } } } diff --git a/iroh/src/ticket/blob.rs b/iroh/src/ticket/blob.rs index 5cbde7f09e..9a33f88522 100644 --- a/iroh/src/ticket/blob.rs +++ b/iroh/src/ticket/blob.rs @@ -31,8 +31,8 @@ impl IrohTicket for Ticket { const KIND: Kind = Kind::Blob; fn verify(&self) -> std::result::Result<(), &'static str> { - if self.node.info.direct_addresses.is_empty() { - return Err("Invalid address list in ticket"); + if self.node.info.is_empty() { + return Err("addressing info cannot be empty"); } Ok(()) } @@ -54,10 +54,7 @@ impl Ticket { format: BlobFormat, token: Option, ) -> Result { - ensure!( - !peer.info.direct_addresses.is_empty(), - "addrs list can not be empty" - ); + ensure!(!peer.info.is_empty(), "addressing info cannot be empty"); Ok(Self { hash, format,