syncweb_core/folder/
syncweb_folder.rs1use 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 capabilities: Arc<RwLock<HashMap<PublicKey, Capability>>>,
39}
40
41impl SyncwebFolder {
42 #[must_use]
43 pub fn new(
44 doc: Doc,
45 author: AuthorId,
46 blob_store: BlobStore,
47 docs_engine: DocsEngine,
48 sync_mode: SyncMode,
49 ) -> Self {
50 let namespace_id = doc.id();
51 Self {
52 doc,
53 namespace_id,
54 author,
55 blob_store,
56 docs_engine,
57 sync_mode,
58 capabilities: Arc::new(RwLock::new(HashMap::new())),
59 }
60 }
61
62 pub async fn drop_namespace(&self) -> Result<()> {
68 self.docs_engine
69 .drop_namespace(self.namespace_id)
70 .await
71 .map_err(|error| SyncwebError::operation("failed to drop folder namespace", error))
72 }
73
74 #[must_use]
75 pub const fn namespace_id(&self) -> NamespaceId {
76 self.namespace_id
77 }
78
79 #[must_use]
80 pub const fn author(&self) -> AuthorId {
81 self.author
82 }
83
84 #[must_use]
85 pub const fn mode(&self) -> SyncMode {
86 self.sync_mode
87 }
88
89 #[must_use]
90 pub const fn doc(&self) -> &Doc {
91 &self.doc
92 }
93
94 #[must_use]
95 pub const fn docs_engine(&self) -> &DocsEngine {
96 &self.docs_engine
97 }
98
99 pub async fn grant(&self, node_id: PublicKey, capability: Capability) {
100 self.capabilities.write().await.insert(node_id, capability);
101 }
102
103 pub async fn capability(&self, node_id: PublicKey) -> Option<Capability> {
104 self.capabilities.read().await.get(&node_id).copied()
105 }
106
107 pub async fn can_write_as(&self, node_id: PublicKey) -> bool {
108 self.sync_mode.can_write_locally() && self.capability(node_id).await.is_some_and(Capability::can_write)
109 }
110
111 pub async fn has_local(&self, hash: Hash) -> Result<bool> {
117 self.blob_store.has(hash).await
118 }
119
120 pub async fn set_metadata_only(&self) -> Result<()> {
128 self.set_download_policy(iroh_docs::store::DownloadPolicy::NothingExcept(Vec::new()))
129 .await
130 }
131
132 pub async fn clear_metadata_only(&self) -> Result<()> {
139 self.set_download_policy(iroh_docs::store::DownloadPolicy::EverythingExcept(Vec::new()))
140 .await
141 }
142
143 pub async fn is_metadata_only(&self) -> Result<bool> {
150 let policy = self
151 .doc
152 .get_download_policy()
153 .await
154 .map_err(|error| SyncwebError::operation("failed to read folder download policy", error))?;
155 Ok(matches!(policy, iroh_docs::store::DownloadPolicy::NothingExcept(_)))
156 }
157
158 async fn set_download_policy(&self, policy: iroh_docs::store::DownloadPolicy) -> Result<()> {
159 self.doc
160 .set_download_policy(policy)
161 .await
162 .map_err(|error| SyncwebError::operation("failed to set folder download policy", error))
163 }
164
165 pub async fn content_entries(&self) -> Result<Vec<iroh_docs::Entry>> {
174 let entries = self.docs_engine.list_latest(self.doc()).await?;
175 Ok(entries
176 .into_iter()
177 .filter(|entry| !entry.key().starts_with(b"sys/"))
178 .collect())
179 }
180
181 pub async fn set_blob(&self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Hash> {
185 if !self.sync_mode.can_write_locally() {
186 return Err(SyncwebError::WriteDenied {
187 mode: self.sync_mode.to_string(),
188 });
189 }
190 let value_bytes = value.as_ref();
191 let hash = self.blob_store.add_bytes(value_bytes).await?;
192 let len = u64::try_from(value_bytes.len())
193 .map_err(|error| SyncwebError::operation("blob size exceeds u64::MAX", error))?;
194 self.docs_engine
195 .set_blob(&self.doc, self.author, key, hash, len)
196 .await?;
197 Ok(hash)
198 }
199
200 pub async fn set_blob_ref(&self, key: impl AsRef<[u8]>, hash: Hash, size: u64) -> Result<()> {
207 if !self.sync_mode.can_write_locally() {
208 return Err(SyncwebError::WriteDenied {
209 mode: self.sync_mode.to_string(),
210 });
211 }
212 self.docs_engine.set_blob(&self.doc, self.author, key, hash, size).await
213 }
214
215 pub async fn delete_entry(&self, key: impl AsRef<[u8]>) -> Result<()> {
221 if !self.sync_mode.can_write_locally() {
222 return Err(SyncwebError::WriteDenied {
223 mode: self.sync_mode.to_string(),
224 });
225 }
226 self.docs_engine.delete(&self.doc, self.author, key).await
227 }
228
229 pub async fn ticket(&self, writable: bool) -> Result<DocTicket> {
233 let can_write = writable && self.sync_mode.can_grant_write();
234 self.docs_engine.share_ticket(&self.doc, can_write).await
235 }
236
237 pub async fn publish_blob(&self, endpoint: iroh::EndpointAddr, hash: Hash) -> Result<BlobTicket> {
244 if !self.blob_store.has(hash).await? {
245 return Err(SyncwebError::InvalidConfig(format!(
246 "cannot publish missing blob {hash}"
247 )));
248 }
249 let ticket = self.blob_store.ticket_for_addr(endpoint, hash);
250 self.blob_store
251 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
252 .await?;
253 Ok(ticket)
254 }
255
256 pub async fn unpublish_blob(&self, hash: Hash) -> Result<()> {
265 self.blob_store
266 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
267 .await
268 }
269
270 pub async fn pin_all_content(&self) -> Result<usize> {
279 let entries = self.docs_engine.list_latest(&self.doc).await?;
280 let mut pinned = 0_usize;
281 for entry in entries {
282 let hash = entry.content_hash();
283 self.blob_store
284 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
285 .await?;
286 pinned = pinned.saturating_add(1);
287 }
288 Ok(pinned)
289 }
290
291 pub async fn unpin_all_content(&self) -> Result<usize> {
300 let entries = self.docs_engine.list_latest(&self.doc).await?;
301 let mut unpinned = 0_usize;
302 for entry in entries {
303 let hash = entry.content_hash();
304 self.blob_store
305 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
306 .await?;
307 unpinned = unpinned.saturating_add(1);
308 }
309 Ok(unpinned)
310 }
311}
312
313#[async_trait]
314impl FolderLike for SyncwebFolder {
315 fn namespace_id(&self) -> String {
316 self.namespace_id.to_string()
317 }
318
319 fn label(&self) -> String {
320 self.namespace_id.to_string()
321 }
322
323 fn kind(&self) -> &'static str {
324 "folder"
325 }
326
327 fn path(&self) -> Option<&Path> {
328 None
329 }
330
331 async fn list_entries(&self) -> Result<Vec<EntryLike>> {
332 let entries = self.docs_engine.list_latest(&self.doc).await?;
333 let mut result = Vec::with_capacity(entries.len());
334 for entry in entries {
335 if entry.key().starts_with(b"sys/") {
336 continue;
337 }
338 let path = String::from_utf8_lossy(entry.key()).to_string();
339 result.push(EntryLike {
340 path,
341 hash: entry.content_hash(),
342 size: entry.content_len(),
343 });
344 }
345 Ok(result)
346 }
347}