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 content_entries(&self) -> Result<Vec<iroh_docs::Entry>> {
129 let entries = self.docs_engine.list_latest(self.doc()).await?;
130 Ok(entries
131 .into_iter()
132 .filter(|entry| !entry.key().starts_with(b"sys/"))
133 .collect())
134 }
135
136 pub async fn set_blob(&self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Hash> {
140 if !self.sync_mode.can_write_locally() {
141 return Err(SyncwebError::WriteDenied {
142 mode: self.sync_mode.to_string(),
143 });
144 }
145 let value_bytes = value.as_ref();
146 let hash = self.blob_store.add_bytes(value_bytes).await?;
147 let len = u64::try_from(value_bytes.len())
148 .map_err(|error| SyncwebError::operation("blob size exceeds u64::MAX", error))?;
149 self.docs_engine
150 .set_blob(&self.doc, self.author, key, hash, len)
151 .await?;
152 Ok(hash)
153 }
154
155 pub async fn set_blob_ref(&self, key: impl AsRef<[u8]>, hash: Hash, size: u64) -> Result<()> {
162 if !self.sync_mode.can_write_locally() {
163 return Err(SyncwebError::WriteDenied {
164 mode: self.sync_mode.to_string(),
165 });
166 }
167 self.docs_engine.set_blob(&self.doc, self.author, key, hash, size).await
168 }
169
170 pub async fn delete_entry(&self, key: impl AsRef<[u8]>) -> Result<()> {
176 if !self.sync_mode.can_write_locally() {
177 return Err(SyncwebError::WriteDenied {
178 mode: self.sync_mode.to_string(),
179 });
180 }
181 self.docs_engine.delete(&self.doc, self.author, key).await
182 }
183
184 pub async fn ticket(&self, writable: bool) -> Result<DocTicket> {
188 let can_write = writable && self.sync_mode.can_grant_write();
189 self.docs_engine.share_ticket(&self.doc, can_write).await
190 }
191
192 pub async fn publish_blob(&self, endpoint: iroh::EndpointAddr, hash: Hash) -> Result<BlobTicket> {
199 if !self.blob_store.has(hash).await? {
200 return Err(SyncwebError::InvalidConfig(format!(
201 "cannot publish missing blob {hash}"
202 )));
203 }
204 let ticket = self.blob_store.ticket_for_addr(endpoint, hash);
205 self.blob_store
206 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
207 .await?;
208 Ok(ticket)
209 }
210
211 pub async fn unpublish_blob(&self, hash: Hash) -> Result<()> {
220 self.blob_store
221 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
222 .await
223 }
224
225 pub async fn pin_all_content(&self) -> Result<usize> {
234 let entries = self.docs_engine.list_latest(&self.doc).await?;
235 let mut pinned = 0_usize;
236 for entry in entries {
237 let hash = entry.content_hash();
238 self.blob_store
239 .pin(crate::pins::public_blob_pin(self.namespace_id, hash), hash)
240 .await?;
241 pinned = pinned.saturating_add(1);
242 }
243 Ok(pinned)
244 }
245
246 pub async fn unpin_all_content(&self) -> Result<usize> {
255 let entries = self.docs_engine.list_latest(&self.doc).await?;
256 let mut unpinned = 0_usize;
257 for entry in entries {
258 let hash = entry.content_hash();
259 self.blob_store
260 .unpin(crate::pins::public_blob_pin(self.namespace_id, hash))
261 .await?;
262 unpinned = unpinned.saturating_add(1);
263 }
264 Ok(unpinned)
265 }
266}
267
268#[async_trait]
269impl FolderLike for SyncwebFolder {
270 fn namespace_id(&self) -> String {
271 self.namespace_id.to_string()
272 }
273
274 fn label(&self) -> String {
275 self.namespace_id.to_string()
276 }
277
278 fn kind(&self) -> &'static str {
279 "folder"
280 }
281
282 fn path(&self) -> Option<&Path> {
283 None
284 }
285
286 async fn list_entries(&self) -> Result<Vec<EntryLike>> {
287 let entries = self.docs_engine.list_latest(&self.doc).await?;
288 let mut result = Vec::with_capacity(entries.len());
289 for entry in entries {
290 if entry.key().starts_with(b"sys/") {
291 continue;
292 }
293 let path = String::from_utf8_lossy(entry.key()).to_string();
294 result.push(EntryLike {
295 path,
296 hash: entry.content_hash(),
297 size: entry.content_len(),
298 });
299 }
300 Ok(result)
301 }
302}