1use std::{collections::HashMap, path::Path, sync::Arc};
2
3use async_trait::async_trait;
4use iroh::PublicKey;
5use iroh_blobs::{Hash, ticket::BlobTicket};
6use iroh_docs::{AuthorId, DocTicket, NamespaceId, api::Doc};
7use tokio::sync::RwLock;
8
9use crate::error::{Result, SyncwebError};
10use crate::node::{blob_store::BlobStore, docs_engine::DocsEngine};
11
12use super::SyncMode;
13use super::public_subscription::{EntryLike, FolderLike};
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[non_exhaustive]
17pub enum Capability {
18 Admin,
19 Write,
20 Read,
21}
22
23impl Capability {
24 #[must_use]
25 pub const fn can_write(self) -> bool {
26 matches!(self, Self::Admin | Self::Write)
27 }
28}
29
30#[derive(Clone)]
31pub struct SyncwebFolder {
32 doc: Doc,
33 namespace_id: NamespaceId,
34 author: AuthorId,
35 blob_store: BlobStore,
36 docs_engine: DocsEngine,
37 sync_mode: SyncMode,
38 writable: bool,
44 capabilities: Arc<RwLock<HashMap<PublicKey, Capability>>>,
45}
46
47impl SyncwebFolder {
48 #[must_use]
49 pub fn new(
50 doc: Doc,
51 author: AuthorId,
52 blob_store: BlobStore,
53 docs_engine: DocsEngine,
54 sync_mode: SyncMode,
55 writable: bool,
56 ) -> Self {
57 let namespace_id = doc.id();
58 Self {
59 doc,
60 namespace_id,
61 author,
62 blob_store,
63 docs_engine,
64 sync_mode,
65 writable,
66 capabilities: Arc::new(RwLock::new(HashMap::new())),
67 }
68 }
69
70 #[must_use]
73 pub const fn is_writable(&self) -> bool {
74 self.writable
75 }
76
77 pub async fn drop_namespace(&self) -> Result<()> {
83 self.docs_engine
84 .drop_namespace(self.namespace_id)
85 .await
86 .map_err(|error| SyncwebError::operation("failed to drop folder namespace", error))
87 }
88
89 #[must_use]
90 pub const fn namespace_id(&self) -> NamespaceId {
91 self.namespace_id
92 }
93
94 #[must_use]
95 pub const fn author(&self) -> AuthorId {
96 self.author
97 }
98
99 #[must_use]
100 pub const fn mode(&self) -> SyncMode {
101 self.sync_mode
102 }
103
104 #[must_use]
105 pub const fn doc(&self) -> &Doc {
106 &self.doc
107 }
108
109 #[must_use]
110 pub const fn docs_engine(&self) -> &DocsEngine {
111 &self.docs_engine
112 }
113
114 pub async fn grant(&self, node_id: PublicKey, capability: Capability) {
115 self.capabilities.write().await.insert(node_id, capability);
116 }
117
118 pub async fn capability(&self, node_id: PublicKey) -> Option<Capability> {
119 self.capabilities.read().await.get(&node_id).copied()
120 }
121
122 pub async fn can_write_as(&self, node_id: PublicKey) -> bool {
123 self.sync_mode.can_write_locally() && self.capability(node_id).await.is_some_and(Capability::can_write)
124 }
125
126 pub async fn has_local(&self, hash: Hash) -> Result<bool> {
132 self.blob_store.has(hash).await
133 }
134
135 pub async fn set_metadata_only(&self) -> Result<()> {
143 self.set_download_policy(iroh_docs::store::DownloadPolicy::NothingExcept(Vec::new()))
144 .await
145 }
146
147 pub async fn clear_metadata_only(&self) -> Result<()> {
154 self.set_download_policy(iroh_docs::store::DownloadPolicy::EverythingExcept(Vec::new()))
155 .await
156 }
157
158 pub async fn is_metadata_only(&self) -> Result<bool> {
165 let policy = self
166 .doc
167 .get_download_policy()
168 .await
169 .map_err(|error| SyncwebError::operation("failed to read folder download policy", error))?;
170 Ok(matches!(policy, iroh_docs::store::DownloadPolicy::NothingExcept(_)))
171 }
172
173 async fn set_download_policy(&self, policy: iroh_docs::store::DownloadPolicy) -> Result<()> {
174 self.doc
175 .set_download_policy(policy)
176 .await
177 .map_err(|error| SyncwebError::operation("failed to set folder download policy", error))
178 }
179
180 pub async fn content_entries(&self) -> Result<Vec<iroh_docs::Entry>> {
189 let entries = self.docs_engine.list_latest(self.doc()).await?;
190 Ok(entries
191 .into_iter()
192 .filter(|entry| !entry.key().starts_with(b"sys/"))
193 .collect())
194 }
195
196 pub async fn set_blob(&self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Hash> {
200 if !self.sync_mode.can_write_locally() {
201 return Err(SyncwebError::WriteDenied {
202 mode: self.sync_mode.to_string(),
203 });
204 }
205 let value_bytes = value.as_ref();
206 let hash = self.blob_store.add_bytes(value_bytes).await?;
207 let len = u64::try_from(value_bytes.len())
208 .map_err(|error| SyncwebError::operation("blob size exceeds u64::MAX", error))?;
209 self.docs_engine
210 .set_blob(&self.doc, self.author, key, hash, len)
211 .await?;
212 Ok(hash)
213 }
214
215 pub async fn set_blob_ref(&self, key: impl AsRef<[u8]>, hash: Hash, size: u64) -> Result<()> {
222 if !self.sync_mode.can_write_locally() {
223 return Err(SyncwebError::WriteDenied {
224 mode: self.sync_mode.to_string(),
225 });
226 }
227 self.docs_engine.set_blob(&self.doc, self.author, key, hash, size).await
228 }
229
230 pub async fn delete_entry(&self, key: impl AsRef<[u8]>) -> Result<()> {
236 if !self.sync_mode.can_write_locally() {
237 return Err(SyncwebError::WriteDenied {
238 mode: self.sync_mode.to_string(),
239 });
240 }
241 self.docs_engine.delete(&self.doc, self.author, key).await
242 }
243
244 pub async fn ticket(&self, writable: bool) -> Result<DocTicket> {
248 self.ticket_with_options(writable, iroh_docs::api::protocol::AddrInfoOptions::RelayAndAddresses)
249 .await
250 }
251
252 pub async fn ticket_with_options(
256 &self,
257 writable: bool,
258 addr_options: iroh_docs::api::protocol::AddrInfoOptions,
259 ) -> Result<DocTicket> {
260 let can_write = writable && self.sync_mode.can_grant_write();
261 self.docs_engine
262 .share_ticket_with_options(&self.doc, can_write, addr_options)
263 .await
264 }
265
266 pub async fn publish_blob(&self, endpoint: iroh::EndpointAddr, hash: Hash) -> Result<BlobTicket> {
273 if !self.blob_store.has(hash).await? {
274 return Err(SyncwebError::InvalidConfig(format!(
275 "cannot publish missing blob {hash}"
276 )));
277 }
278 let ticket = self.blob_store.ticket_for_addr(endpoint, hash);
279 self.blob_store
280 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
281 .await?;
282 Ok(ticket)
283 }
284
285 pub async fn unpublish_blob(&self, hash: Hash) -> Result<()> {
294 self.blob_store
295 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
296 .await
297 }
298
299 pub async fn pin_all_content(&self) -> Result<usize> {
308 let entries = self.docs_engine.list_latest(&self.doc).await?;
309 let mut pinned = 0_usize;
310 for entry in entries {
311 let hash = entry.content_hash();
312 self.blob_store
313 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
314 .await?;
315 pinned = pinned.saturating_add(1);
316 }
317 Ok(pinned)
318 }
319
320 pub async fn unpin_all_content(&self) -> Result<usize> {
329 let entries = self.docs_engine.list_latest(&self.doc).await?;
330 let mut unpinned = 0_usize;
331 for entry in entries {
332 let hash = entry.content_hash();
333 self.blob_store
334 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
335 .await?;
336 unpinned = unpinned.saturating_add(1);
337 }
338 Ok(unpinned)
339 }
340}
341
342#[async_trait]
343impl FolderLike for SyncwebFolder {
344 fn namespace_id(&self) -> String {
345 self.namespace_id.to_string()
346 }
347
348 fn label(&self) -> String {
349 self.namespace_id.to_string()
350 }
351
352 fn kind(&self) -> &'static str {
353 "folder"
354 }
355
356 fn path(&self) -> Option<&Path> {
357 None
358 }
359
360 async fn list_entries(&self) -> Result<Vec<EntryLike>> {
361 let entries = self.docs_engine.list_latest(&self.doc).await?;
362 let mut result = Vec::with_capacity(entries.len());
363 for entry in entries {
364 if entry.key().starts_with(b"sys/") {
365 continue;
366 }
367 let path = String::from_utf8_lossy(entry.key()).to_string();
368 result.push(EntryLike {
369 path,
370 hash: entry.content_hash(),
371 size: entry.content_len(),
372 });
373 }
374 Ok(result)
375 }
376}