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, PathBuf};
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 let path = absolute_path(path)?;
69 Ok(self
70 .store
71 .add_path(path)
72 .await
73 .map_err(|error| SyncwebError::operation("failed to add blob file", error))?
74 .hash)
75 }
76
77 pub async fn add_file_ref(&self, path: impl AsRef<Path>) -> Result<Hash> {
87 let path = absolute_path(path)?;
88 Ok(self
89 .store
90 .add_path_with_opts(AddPathOptions {
91 path,
92 mode: ImportMode::TryReference,
93 format: BlobFormat::Raw,
94 })
95 .await
96 .map_err(|error| SyncwebError::operation("failed to add blob file (reference)", error))?
97 .hash)
98 }
99
100 pub async fn export_to_path(&self, hash: Hash, destination: impl AsRef<Path>) -> Result<u64> {
109 self.export_to_path_with_mode(hash, destination, ExportMode::Copy).await
110 }
111
112 pub async fn export_to_path_with_mode(
123 &self,
124 hash: Hash,
125 destination: impl AsRef<Path>,
126 mode: ExportMode,
127 ) -> Result<u64> {
128 self.store
129 .export_with_opts(ExportOptions {
130 hash,
131 mode,
132 target: destination.as_ref().to_owned(),
133 })
134 .await
135 .map_err(|error| SyncwebError::operation("failed to export blob", error))
136 }
137
138 pub async fn has(&self, hash: Hash) -> Result<bool> {
142 self.store
143 .has(hash)
144 .await
145 .map_err(|error| SyncwebError::operation("failed to query blob store", error))
146 }
147
148 pub async fn stat(&self, hash: Hash) -> Result<BlobStatus> {
154 self.store
155 .blobs()
156 .status(hash)
157 .await
158 .map_err(|error| SyncwebError::operation("failed to stat blob", error))
159 }
160
161 pub async fn get(&self, hash: Hash) -> Result<Bytes> {
165 self.store
166 .get_bytes(hash)
167 .await
168 .map_err(|error| SyncwebError::operation("failed to read blob", error))
169 }
170
171 #[must_use]
172 pub fn ticket(&self, endpoint: &Endpoint, hash: Hash) -> BlobTicket {
173 self.ticket_for_addr(endpoint.addr(), hash)
174 }
175
176 #[must_use]
177 pub fn ticket_for_addr(&self, addr: EndpointAddr, hash: Hash) -> BlobTicket {
178 BlobTicket::new(addr, hash, iroh_blobs::BlobFormat::Raw)
179 }
180
181 pub async fn pin(&self, name: impl AsRef<str>, hash: Hash) -> Result<()> {
187 self.store
188 .tags()
189 .set(name.as_ref(), hash)
190 .await
191 .map_err(|error| SyncwebError::operation("failed to pin blob", error))
192 }
193
194 pub async fn unpin(&self, name: impl AsRef<str>) -> Result<()> {
200 self.store
201 .tags()
202 .delete(name.as_ref())
203 .await
204 .map(|_deleted| ())
205 .map_err(|error| SyncwebError::operation("failed to unpin blob", error))
206 }
207
208 pub async fn is_pinned(&self, name: impl AsRef<str>, hash: Hash) -> Result<bool> {
212 self.store
213 .tags()
214 .get(name.as_ref())
215 .await
216 .map(|tag_info| tag_info.is_some_and(|tag| tag.hash == hash))
217 .map_err(|error| SyncwebError::operation("failed to query blob pin", error))
218 }
219
220 pub async fn list_hashes(&self) -> Result<Vec<Hash>> {
226 self.store
227 .blobs()
228 .list()
229 .hashes()
230 .await
231 .map_err(|error| SyncwebError::operation("failed to list blobs", error))
232 }
233
234 pub async fn list_pins(&self, prefix: impl AsRef<[u8]>) -> Result<Vec<(String, Hash)>> {
240 let mut tags = self
241 .store
242 .tags()
243 .list_prefix(prefix)
244 .await
245 .map_err(|error| SyncwebError::operation("failed to list blob pins", error))?;
246 let mut pins = Vec::new();
247 while let Some(tag_result) = tags.next().await {
248 let tag = tag_result.map_err(|error| SyncwebError::operation("failed to read blob pin", error))?;
249 let name = String::from_utf8(tag.name.as_ref().to_vec())
250 .map_err(|error| SyncwebError::operation("blob pin name is not UTF-8", error))?;
251 pins.push((name, tag.hash));
252 }
253 Ok(pins)
254 }
255
256 pub async fn fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
260 self.address_lookup.add_endpoint_info(ticket.addr().clone());
261 self.store
262 .downloader(endpoint)
263 .download(ticket.hash_and_format(), [ticket.addr().id])
264 .await
265 .map_err(|error| SyncwebError::operation("failed to fetch blob", error))?;
266 Ok(())
267 }
268
269 pub async fn download_with_network(
279 &self,
280 endpoint: &Endpoint,
281 ticket: &BlobTicket,
282 ctx: &NetworkContext,
283 ) -> Result<()> {
284 let hash = ticket.hash();
285 if !ctx.can_access_blob(&hash)? {
286 return Err(SyncwebError::access_denied("blob not accessible in any network"));
287 }
288 self.fetch(endpoint, ticket).await
289 }
290
291 pub async fn force_fetch(&self, endpoint: &Endpoint, ticket: &BlobTicket) -> Result<()> {
299 self.address_lookup.add_endpoint_info(ticket.addr().clone());
300 let hash = ticket.hash();
301 let conn = endpoint
302 .connect(ticket.addr().id, iroh_blobs::protocol::ALPN)
303 .await
304 .map_err(|error| SyncwebError::operation("failed to connect to provider", error))?;
305 self.force_fetch_from_conn(conn, hash).await
306 }
307
308 pub async fn force_fetch_from_peer(&self, endpoint: &Endpoint, peer: &iroh::PublicKey, hash: Hash) -> Result<()> {
314 let conn = endpoint
315 .connect(*peer, iroh_blobs::protocol::ALPN)
316 .await
317 .map_err(|error| SyncwebError::operation("failed to connect to peer", error))?;
318 self.force_fetch_from_conn(conn, hash).await
319 }
320
321 async fn force_fetch_from_conn(&self, conn: iroh::endpoint::Connection, hash: Hash) -> Result<()> {
322 let request = GetRequest::blob(hash);
323 self.store
324 .remote()
325 .execute_get(conn, request)
326 .await
327 .map_err(|error| SyncwebError::operation("failed to force-fetch blob", error))?;
328 Ok(())
329 }
330}
331
332fn absolute_path(path: impl AsRef<Path>) -> Result<PathBuf> {
333 std::path::absolute(path.as_ref())
334 .map_err(|error| SyncwebError::operation("failed to resolve blob file path", error))
335}