Milestone 3: iroh endpoint, tickets, pin-triggered fetch
The daemon binds an iroh endpoint (ed25519 identity at secret.key, 0600), serves its store via Blobs/Router on the standard ALPN, and fetches with the iroh-blobs Downloader rather than the rpc client to keep quic-rpc out of the tree. Relay is disabled unless wan_upload is set: LAN-only, zero WAN upload by default. TicketImport pins first (the pin is the GC root protecting in-flight data), registers the ticket's NodeAddr with the endpoint (the downloader dials by NodeId alone), then fetches in the background; completion emits pin_complete. Tests: two-daemon localhost transfers (blob + directory collection, byte-identical), and kill -9 mid-transfer followed by restart on the same store resuming to completion. Dependencies: iroh 0.35 (endpoint/router, pairs with iroh-blobs 0.35), rand 0.8 (secret key generation, same version iroh uses). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use varde_proto::{
|
||||
use crate::config::Config;
|
||||
use crate::meta::{Meta, StoredFormat};
|
||||
use crate::store::{BlobStore, OpError};
|
||||
use crate::transfer::Transfer;
|
||||
|
||||
/// Shared daemon state. Handlers grow with the milestones; anything not
|
||||
/// yet wired returns [`ErrorCode::Unimplemented`] rather than pretending.
|
||||
@@ -20,11 +21,13 @@ pub struct Daemon {
|
||||
config: Config,
|
||||
store: BlobStore,
|
||||
meta: Meta,
|
||||
transfer: Transfer,
|
||||
events: broadcast::Sender<Event>,
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
/// Open the blob store and metadata under the configured store dir.
|
||||
/// Open the blob store and metadata under the configured store dir,
|
||||
/// and bring up the iroh endpoint.
|
||||
pub async fn open(config: Config) -> anyhow::Result<Daemon> {
|
||||
std::fs::create_dir_all(&config.store_dir)
|
||||
.with_context(|| format!("creating store directory {}", config.store_dir.display()))?;
|
||||
@@ -33,14 +36,23 @@ impl Daemon {
|
||||
// Capacity bounds memory if a subscriber stalls; laggards get a
|
||||
// Lagged error, not unbounded buffering.
|
||||
let (events, _) = broadcast::channel(1024);
|
||||
let transfer =
|
||||
Transfer::start(&config, &store, store.pool_handle(), events.clone()).await?;
|
||||
Ok(Daemon {
|
||||
config,
|
||||
store,
|
||||
meta,
|
||||
transfer,
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
/// Flush the store and close the endpoint.
|
||||
pub async fn shutdown(&self) {
|
||||
self.transfer.shutdown().await;
|
||||
self.store.shutdown().await;
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
@@ -106,7 +118,7 @@ impl Daemon {
|
||||
Request::Status { hash: None } => Ok(ResponseData::Status(GlobalStatus {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
protocol: varde_proto::PROTOCOL_VERSION,
|
||||
node_id: None,
|
||||
node_id: Some(self.transfer.node_id()),
|
||||
store_path: self.config.store_dir.display().to_string(),
|
||||
pins: self.meta.pins().len() as u64,
|
||||
connected_peers: 0,
|
||||
@@ -143,6 +155,40 @@ impl Daemon {
|
||||
let blobs_removed = self.store.gc(roots).await?;
|
||||
Ok(ResponseData::GcDone { blobs_removed })
|
||||
}
|
||||
Request::TicketExport { hash } => {
|
||||
let parsed = parse_hash(&hash)?;
|
||||
let (_have, _total, complete) = self.store.presence(parsed).await?;
|
||||
if !complete {
|
||||
return Err(OpError::NotFound(format!(
|
||||
"{} is not fully present; cannot export a ticket",
|
||||
parsed.to_hex()
|
||||
)));
|
||||
}
|
||||
let content = HashAndFormat {
|
||||
hash: parsed,
|
||||
format: blob_format(self.meta.format_of(&parsed.to_hex())),
|
||||
};
|
||||
let ticket = self.transfer.export_ticket(content).await?;
|
||||
Ok(ResponseData::Ticket { ticket })
|
||||
}
|
||||
Request::TicketImport { ticket, pin_policy } => {
|
||||
let (hash, format, provider) =
|
||||
crate::transfer::parse_ticket(&ticket).map_err(OpError::InvalidArgument)?;
|
||||
let stored = match format {
|
||||
BlobFormat::Raw => StoredFormat::Raw,
|
||||
BlobFormat::HashSeq => StoredFormat::HashSeq,
|
||||
};
|
||||
self.meta.record_format(&hash.to_hex(), stored)?;
|
||||
// Pin first: the pin is the GC root that keeps the fetch's
|
||||
// data alive, and it is the standing intent even if the
|
||||
// provider is unreachable right now.
|
||||
self.meta.pin(&hash.to_hex(), pin_policy)?;
|
||||
self.transfer
|
||||
.spawn_fetch(HashAndFormat { hash, format }, vec![provider]);
|
||||
Ok(ResponseData::Pinned {
|
||||
hash: hash.to_hex().to_string(),
|
||||
})
|
||||
}
|
||||
Request::Subscribe {} => Ok(ResponseData::Done {}),
|
||||
other => Err(OpError::Unimplemented(format!(
|
||||
"not implemented yet: {other:?}"
|
||||
@@ -155,19 +201,22 @@ impl Daemon {
|
||||
let mut roots = Vec::new();
|
||||
for (hash, _record) in self.meta.pins() {
|
||||
let parsed = parse_hash(&hash)?;
|
||||
let format = match self.meta.format_of(&hash) {
|
||||
StoredFormat::Raw => BlobFormat::Raw,
|
||||
StoredFormat::HashSeq => BlobFormat::HashSeq,
|
||||
};
|
||||
roots.push(HashAndFormat {
|
||||
hash: parsed,
|
||||
format,
|
||||
format: blob_format(self.meta.format_of(&hash)),
|
||||
});
|
||||
}
|
||||
Ok(roots)
|
||||
}
|
||||
}
|
||||
|
||||
fn blob_format(format: StoredFormat) -> BlobFormat {
|
||||
match format {
|
||||
StoredFormat::Raw => BlobFormat::Raw,
|
||||
StoredFormat::HashSeq => BlobFormat::HashSeq,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hash(s: &str) -> Result<Hash, OpError> {
|
||||
Hash::from_str(s).map_err(|e| OpError::InvalidArgument(format!("invalid hash {s:?}: {e}")))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user