use bytes::Bytes;
use iroh::{EndpointAddr, address_lookup::memory::MemoryLookup, endpoint::Endpoint};
use iroh_blobs::{
BlobFormat, BlobsProtocol, Hash,
api::{
Store as BlobApi,
blobs::{AddPathOptions, BlobReader, BlobStatus, ExportMode, ExportOptions, ImportMode},
},
protocol::GetRequest,
ticket::BlobTicket,
};
use n0_future::StreamExt;
use std::path::{Path, PathBuf};
use crate::error::{Result, SyncwebError};
#[derive(Clone)]
pub struct BlobStore {
store: BlobApi,
address_lookup: MemoryLookup,
}
impl BlobStore {
#[must_use]
pub fn new(protocol: &BlobsProtocol) -> Self {
Self::new_with_address_lookup(protocol, MemoryLookup::new())
}
#[must_use]
pub fn new_with_address_lookup(protocol: &BlobsProtocol, address_lookup: MemoryLookup) -> Self {
Self {
store: protocol.store().clone(),
address_lookup,
}
}
#[must_use]
pub const fn inner(&self) -> &BlobApi {
&self.store
}
#[must_use]
pub fn reader(&self, hash: impl Into<Hash>) -> BlobReader {
self.store.reader(hash)
}
pub async fn add_bytes(&self, data: impl AsRef<[u8]>) -> Result<Hash> {
Ok(self
.store
.add_bytes(Bytes::copy_from_slice(data.as_ref()))
.await
.map_err(|error| SyncwebError::operation("failed to add blob bytes", error))?
.hash)
}
pub async fn add_file(&self, path: impl AsRef<Path>) -> Result<Hash> {
let absolute_path = absolute_path(path)?;
Ok(self
.store
.add_path(absolute_path)
.await
.map_err(|error| SyncwebError::operation("failed to add blob file", error))?
.hash)
}
pub async fn add_file_ref(&self, path: impl AsRef<Path>) -> Result<Hash> {
let absolute_path = absolute_path(path)?;
Ok(self
.store
.add_path_with_opts(AddPathOptions {
path: absolute_path,
mode: ImportMode::TryReference,
format: BlobFormat::Raw,
})
.await
.map_err(|error| SyncwebError::operation("failed to add blob file (reference)", error))?
.hash)
}
pub async fn export_to_path(&self, hash: Hash, destination: impl AsRef<Path>) -> Result<u64> {
self.export_to_path_with_mode(hash, destination, ExportMode::Copy).await
}
pub async fn export_to_path_with_mode(
&self,
hash: Hash,
destination: impl AsRef<Path>,
mode: ExportMode,
) -> Result<u64> {
self.store
.export_with_opts(ExportOptions {
hash,
mode,
target: destination.as_ref().to_owned(),
})
.await
.map_err(|error| SyncwebError::operation("failed to export blob", error))
}
pub async fn has(&self, hash: Hash) -> Result<bool> {
self.store
.has(hash)
.await
.map_err(|error| SyncwebError::operation("failed to query blob store", error))
}
pub async fn stat(&self, hash: Hash) -> Result<BlobStatus> {
self.store
.blobs()
.status(hash)
.await
.map_err(|error| SyncwebError::operation("failed to stat blob", error))
}
pub async fn get(&self, hash: Hash) -> Result<Bytes> {
self.store
.get_bytes(hash)
.await
.map_err(|error| SyncwebError::operation("failed to read blob", error))
}
#[must_use]
pub fn ticket(&self, endpoint: &Endpoint, hash: Hash) -> BlobTicket {
self.ticket_for_addr(endpoint.addr(), hash)
}
#[must_use]
pub fn ticket_for_addr(&self, addr: EndpointAddr, hash: Hash) -> BlobTicket {
raw_blob_ticket(addr, hash)
}
pub async fn pin(&self, name: impl AsRef<str>, hash: Hash) -> Result<()> {
self.store
.tags()
.set(name.as_ref(), hash)
.await
.map_err(|error| SyncwebError::operation("failed to pin blob", error))
}
pub async fn unpin(&self, name: impl AsRef<str>) -> Result<()> {
self.store
.tags()
.delete(name.as_ref())
.await
.map(|_deleted| ())
.map_err(|error| SyncwebError::operation("failed to unpin blob", error))
}
pub async fn is_pinned(&self, name: impl AsRef<str>, hash: Hash) -> Result<bool> {
self.store
.tags()
.get(name.as_ref())
.await
.map(|tag_info| tag_info.is_some_and(|tag| tag.hash == hash))
.map_err(|error| SyncwebError::operation("failed to query blob pin", error))
}
pub async fn list_hashes(&self) -> Result<Vec<Hash>> {
self.store
.blobs()
.list()
.hashes()
.await
.map_err(|error| SyncwebError::operation("failed to list blobs", error))
}
pub async fn list_pins(&self, prefix: impl AsRef<[u8]>) -> Result<Vec<(String, Hash)>> {
let mut tags = self
.store
.tags()
.list_prefix(prefix)
.await
.map_err(|error| SyncwebError::operation("failed to list blob pins", error))?;
let mut pins = Vec::new();
while let Some(tag_result) = tags.next().await {
let tag = tag_result.map_err(|error| SyncwebError::operation("failed to read blob pin", error))?;
let name = String::from_utf8(tag.name.as_ref().to_vec())
.map_err(|error| SyncwebError::operation("blob pin name is not UTF-8", error))?;
pins.push((name, tag.hash));
}
Ok(pins)
}
pub async fn fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
self.address_lookup.add_endpoint_info(ticket.addr().clone());
self.store
.downloader(endpoint)
.download(ticket.hash_and_format(), [ticket.addr().id])
.await
.map_err(|error| SyncwebError::operation("failed to fetch blob", error))?;
Ok(())
}
pub async fn fetch_and_pin(&self, endpoint: &Endpoint, content_hash: Hash, tickets: &[BlobTicket]) -> Result<()> {
if !self.has(content_hash).await? {
let candidates: Vec<BlobTicket> = tickets
.iter()
.filter(|ticket| ticket.hash() == content_hash)
.cloned()
.collect();
try_fetch_first_working(&candidates, |ticket| self.fetch(endpoint, ticket)).await?;
}
let pin_name = crate::pins::download_pin(content_hash);
self.pin(&pin_name, content_hash).await?;
Ok(())
}
pub async fn force_fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
self.address_lookup.add_endpoint_info(ticket.addr().clone());
let hash = ticket.hash();
let conn = endpoint
.connect(ticket.addr().id, iroh_blobs::protocol::ALPN)
.await
.map_err(|error| SyncwebError::operation("failed to connect to provider", error))?;
self.force_fetch_from_conn(conn, hash).await
}
pub async fn force_fetch_from_peer(&self, endpoint: &Endpoint, peer: &iroh::PublicKey, hash: Hash) -> Result<()> {
let conn = endpoint
.connect(*peer, iroh_blobs::protocol::ALPN)
.await
.map_err(|error| SyncwebError::operation("failed to connect to peer", error))?;
self.force_fetch_from_conn(conn, hash).await
}
async fn force_fetch_from_conn(&self, conn: iroh::endpoint::Connection, hash: Hash) -> Result<()> {
let request = GetRequest::blob(hash);
self.store
.remote()
.execute_get(conn, request)
.await
.map_err(|error| SyncwebError::operation("failed to force-fetch blob", error))?;
Ok(())
}
}
fn absolute_path(path: impl AsRef<Path>) -> Result<PathBuf> {
std::path::absolute(path.as_ref())
.map_err(|error| SyncwebError::operation("failed to resolve blob file path", error))
}
#[must_use]
pub fn raw_blob_ticket(addr: EndpointAddr, hash: Hash) -> BlobTicket {
BlobTicket::new(addr, hash, BlobFormat::Raw)
}
pub(crate) async fn try_fetch_first_working<'a, T, U, F, Fut>(items: &'a [T], mut fetch: F) -> Result<U>
where
T: Sync,
F: FnMut(&'a T) -> Fut + Send,
Fut: std::future::Future<Output = Result<U>> + Send + 'a,
{
let mut last_error: Option<SyncwebError> = None;
for item in items {
match fetch(item).await {
Ok(value) => return Ok(value),
Err(error) => last_error = Some(error),
}
}
Err(last_error.unwrap_or_else(|| SyncwebError::operation("failed to fetch blob", "no working provider ticket")))
}