1use bytes::Bytes;
2use iroh::{EndpointAddr, address_lookup::memory::MemoryLookup, endpoint::Endpoint};
3use iroh_blobs::{
4 BlobFormat, BlobsProtocol, Hash,
5 api::{
6 Store as BlobApi,
7 blobs::{AddPathOptions, BlobReader, BlobStatus, ExportMode, ExportOptions, ImportMode},
8 },
9 protocol::GetRequest,
10 ticket::BlobTicket,
11};
12use n0_future::StreamExt;
13use std::path::Path;
14
15use crate::error::{Result, SyncwebError};
16use crate::net::NetworkContext;
17
18#[derive(Clone)]
19pub struct BlobStore {
20 store: BlobApi,
21 address_lookup: MemoryLookup,
22}
23
24impl BlobStore {
25 #[must_use]
26 pub fn new(protocol: &BlobsProtocol) -> Self {
27 Self::new_with_address_lookup(protocol, MemoryLookup::new())
28 }
29
30 #[must_use]
31 pub fn new_with_address_lookup(protocol: &BlobsProtocol, address_lookup: MemoryLookup) -> Self {
32 Self {
33 store: protocol.store().clone(),
34 address_lookup,
35 }
36 }
37
38 #[must_use]
39 pub const fn inner(&self) -> &BlobApi {
40 &self.store
41 }
42
43 #[must_use]
48 pub fn reader(&self, hash: impl Into<Hash>) -> BlobReader {
49 self.store.reader(hash)
50 }
51
52 pub async fn add_bytes(&self, data: impl AsRef<[u8]>) -> Result<Hash> {
56 Ok(self
57 .store
58 .add_bytes(Bytes::copy_from_slice(data.as_ref()))
59 .await
60 .map_err(|error| SyncwebError::operation("failed to add blob bytes", error))?
61 .hash)
62 }
63
64 pub async fn add_file(&self, path: impl AsRef<Path>) -> Result<Hash> {
68 Ok(self
69 .store
70 .add_path(path)
71 .await
72 .map_err(|error| SyncwebError::operation("failed to add blob file", error))?
73 .hash)
74 }
75
76 pub async fn add_file_ref(&self, path: impl AsRef<Path>) -> Result<Hash> {
86 Ok(self
87 .store
88 .add_path_with_opts(AddPathOptions {
89 path: path.as_ref().to_owned(),
90 mode: ImportMode::TryReference,
91 format: BlobFormat::Raw,
92 })
93 .await
94 .map_err(|error| SyncwebError::operation("failed to add blob file (reference)", error))?
95 .hash)
96 }
97
98 pub async fn export_to_path(&self, hash: Hash, destination: impl AsRef<Path>) -> Result<u64> {
107 self.export_to_path_with_mode(hash, destination, ExportMode::Copy).await
108 }
109
110 pub async fn export_to_path_with_mode(
121 &self,
122 hash: Hash,
123 destination: impl AsRef<Path>,
124 mode: ExportMode,
125 ) -> Result<u64> {
126 self.store
127 .export_with_opts(ExportOptions {
128 hash,
129 mode,
130 target: destination.as_ref().to_owned(),
131 })
132 .await
133 .map_err(|error| SyncwebError::operation("failed to export blob", error))
134 }
135
136 pub async fn has(&self, hash: Hash) -> Result<bool> {
140 self.store
141 .has(hash)
142 .await
143 .map_err(|error| SyncwebError::operation("failed to query blob store", error))
144 }
145
146 pub async fn stat(&self, hash: Hash) -> Result<BlobStatus> {
152 self.store
153 .blobs()
154 .status(hash)
155 .await
156 .map_err(|error| SyncwebError::operation("failed to stat blob", error))
157 }
158
159 pub async fn get(&self, hash: Hash) -> Result<Bytes> {
163 self.store
164 .get_bytes(hash)
165 .await
166 .map_err(|error| SyncwebError::operation("failed to read blob", error))
167 }
168
169 #[must_use]
170 pub fn ticket(&self, endpoint: &Endpoint, hash: Hash) -> BlobTicket {
171 self.ticket_for_addr(endpoint.addr(), hash)
172 }
173
174 #[must_use]
175 pub fn ticket_for_addr(&self, addr: EndpointAddr, hash: Hash) -> BlobTicket {
176 BlobTicket::new(addr, hash, iroh_blobs::BlobFormat::Raw)
177 }
178
179 pub async fn pin(&self, name: impl AsRef<str>, hash: Hash) -> Result<()> {
185 self.store
186 .tags()
187 .set(name.as_ref(), hash)
188 .await
189 .map_err(|error| SyncwebError::operation("failed to pin blob", error))
190 }
191
192 pub async fn unpin(&self, name: impl AsRef<str>) -> Result<()> {
198 self.store
199 .tags()
200 .delete(name.as_ref())
201 .await
202 .map(|_deleted| ())
203 .map_err(|error| SyncwebError::operation("failed to unpin blob", error))
204 }
205
206 pub async fn is_pinned(&self, name: impl AsRef<str>, hash: Hash) -> Result<bool> {
210 self.store
211 .tags()
212 .get(name.as_ref())
213 .await
214 .map(|tag_info| tag_info.is_some_and(|tag| tag.hash == hash))
215 .map_err(|error| SyncwebError::operation("failed to query blob pin", error))
216 }
217
218 pub async fn list_hashes(&self) -> Result<Vec<Hash>> {
224 self.store
225 .blobs()
226 .list()
227 .hashes()
228 .await
229 .map_err(|error| SyncwebError::operation("failed to list blobs", error))
230 }
231
232 pub async fn list_pins(&self, prefix: impl AsRef<[u8]>) -> Result<Vec<(String, Hash)>> {
238 let mut tags = self
239 .store
240 .tags()
241 .list_prefix(prefix)
242 .await
243 .map_err(|error| SyncwebError::operation("failed to list blob pins", error))?;
244 let mut pins = Vec::new();
245 while let Some(tag_result) = tags.next().await {
246 let tag = tag_result.map_err(|error| SyncwebError::operation("failed to read blob pin", error))?;
247 let name = String::from_utf8(tag.name.as_ref().to_vec())
248 .map_err(|error| SyncwebError::operation("blob pin name is not UTF-8", error))?;
249 pins.push((name, tag.hash));
250 }
251 Ok(pins)
252 }
253
254 pub async fn fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
258 self.address_lookup.add_endpoint_info(ticket.addr().clone());
259 self.store
260 .downloader(endpoint)
261 .download(ticket.hash_and_format(), [ticket.addr().id])
262 .await
263 .map_err(|error| SyncwebError::operation("failed to fetch blob", error))?;
264 Ok(())
265 }
266
267 pub async fn download_with_network(
277 &self,
278 endpoint: &Endpoint,
279 ticket: &BlobTicket,
280 ctx: &NetworkContext,
281 ) -> Result<()> {
282 let hash = ticket.hash();
283 if !ctx.can_access_blob(&hash)? {
284 return Err(SyncwebError::access_denied("blob not accessible in any network"));
285 }
286 self.fetch(endpoint, ticket).await
287 }
288
289 pub async fn force_fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
297 self.address_lookup.add_endpoint_info(ticket.addr().clone());
298 let hash = ticket.hash();
299 let conn = endpoint
300 .connect(ticket.addr().id, iroh_blobs::protocol::ALPN)
301 .await
302 .map_err(|error| SyncwebError::operation("failed to connect to provider", error))?;
303 self.force_fetch_from_conn(conn, hash).await
304 }
305
306 pub async fn force_fetch_from_peer(&self, endpoint: &Endpoint, peer: &iroh::PublicKey, hash: Hash) -> Result<()> {
312 let conn = endpoint
313 .connect(*peer, iroh_blobs::protocol::ALPN)
314 .await
315 .map_err(|error| SyncwebError::operation("failed to connect to peer", error))?;
316 self.force_fetch_from_conn(conn, hash).await
317 }
318
319 async fn force_fetch_from_conn(&self, conn: iroh::endpoint::Connection, hash: Hash) -> Result<()> {
320 let request = GetRequest::blob(hash);
321 self.store
322 .remote()
323 .execute_get(conn, request)
324 .await
325 .map_err(|error| SyncwebError::operation("failed to force-fetch blob", error))?;
326 Ok(())
327 }
328}