Skip to main content

syncweb_core/folder/
syncweb_folder.rs

1use std::{collections::HashMap, sync::Arc};
2
3use iroh::PublicKey;
4use iroh_blobs::{Hash, ticket::BlobTicket};
5use iroh_docs::{
6    AuthorId, DocTicket, NamespaceId,
7    api::{Doc, protocol::ShareMode},
8};
9use tokio::sync::RwLock;
10
11use crate::error::{Result, SyncwebError};
12use crate::node::{blob_store::BlobStore, docs_engine::DocsEngine};
13use crate::snapshot::{Snapshot, SnapshotDiff, SnapshotId, SnapshotStore};
14
15use super::SyncMode;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum Capability {
20    Admin,
21    Write,
22    Read,
23}
24
25impl Capability {
26    #[must_use]
27    pub const fn can_write(self) -> bool {
28        matches!(self, Self::Admin | Self::Write)
29    }
30}
31
32#[derive(Clone)]
33pub struct SyncwebFolder {
34    doc: Doc,
35    namespace_id: NamespaceId,
36    author: AuthorId,
37    blob_store: BlobStore,
38    docs_engine: DocsEngine,
39    sync_mode: SyncMode,
40    capabilities: Arc<RwLock<HashMap<PublicKey, Capability>>>,
41}
42
43impl SyncwebFolder {
44    #[must_use]
45    pub fn new(
46        doc: Doc,
47        author: AuthorId,
48        blob_store: BlobStore,
49        docs_engine: DocsEngine,
50        sync_mode: SyncMode,
51    ) -> Self {
52        let namespace_id = doc.id();
53        Self {
54            doc,
55            namespace_id,
56            author,
57            blob_store,
58            docs_engine,
59            sync_mode,
60            capabilities: Arc::new(RwLock::new(HashMap::new())),
61        }
62    }
63
64    /// Create a new folder by allocating a namespace in the docs engine.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the namespace cannot be created or the default author cannot be retrieved.
69    pub async fn create(docs_engine: DocsEngine, blob_store: BlobStore, sync_mode: SyncMode) -> Result<Self> {
70        let doc = docs_engine
71            .create_namespace()
72            .await
73            .map_err(|error| SyncwebError::operation("failed to create folder namespace", error))?;
74        let author = docs_engine
75            .author()
76            .await
77            .map_err(|error| SyncwebError::operation("failed to retrieve folder author", error))?;
78        Ok(Self::new(doc, author, blob_store, docs_engine, sync_mode))
79    }
80
81    /// Accept a locally available folder by namespace ID.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the namespace cannot be opened.
86    pub async fn accept(docs_engine: DocsEngine, blob_store: BlobStore, namespace_id: NamespaceId) -> Result<Self> {
87        let doc = docs_engine
88            .open(namespace_id)
89            .await
90            .map_err(|error| SyncwebError::operation("failed to open folder namespace", error))?
91            .ok_or(SyncwebError::NamespaceNotAvailable)?;
92        let author = docs_engine
93            .author()
94            .await
95            .map_err(|error| SyncwebError::operation("failed to retrieve folder author", error))?;
96        Ok(Self::new(doc, author, blob_store, docs_engine, SyncMode::ReceiveOnly))
97    }
98
99    /// Drop this folder's namespace from the docs engine.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the namespace cannot be dropped.
104    pub async fn drop_namespace(&self) -> Result<()> {
105        self.docs_engine
106            .drop_namespace(self.namespace_id)
107            .await
108            .map_err(|error| SyncwebError::operation("failed to drop folder namespace", error))
109    }
110
111    #[must_use]
112    pub const fn namespace_id(&self) -> NamespaceId {
113        self.namespace_id
114    }
115
116    #[must_use]
117    pub const fn author(&self) -> AuthorId {
118        self.author
119    }
120
121    #[must_use]
122    pub const fn mode(&self) -> SyncMode {
123        self.sync_mode
124    }
125
126    #[must_use]
127    pub const fn doc(&self) -> &Doc {
128        &self.doc
129    }
130
131    #[must_use]
132    pub const fn docs_engine(&self) -> &DocsEngine {
133        &self.docs_engine
134    }
135
136    pub async fn grant(&self, node_id: PublicKey, capability: Capability) {
137        self.capabilities.write().await.insert(node_id, capability);
138    }
139
140    pub async fn capability(&self, node_id: PublicKey) -> Option<Capability> {
141        self.capabilities.read().await.get(&node_id).copied()
142    }
143
144    pub async fn can_write_as(&self, node_id: PublicKey) -> bool {
145        self.sync_mode.can_write() && self.capability(node_id).await.is_some_and(Capability::can_write)
146    }
147
148    /// Check whether a blob is complete in this folder's local store.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the blob store cannot be queried.
153    pub async fn has_local(&self, hash: Hash) -> Result<bool> {
154        self.blob_store.has(hash).await
155    }
156
157    /// Create a content-addressed snapshot of this folder.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if document entries cannot be read or referenced blobs
162    /// are unavailable.
163    pub async fn create_snapshot(&self, description: Option<String>) -> Result<Snapshot> {
164        SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
165            .create_for_folder(self, description)
166            .await
167    }
168
169    /// Restore this folder's document entries from a snapshot.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the snapshot belongs to another folder or content
174    /// is unavailable.
175    pub async fn restore_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
176        SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
177            .restore_for_folder(self, snapshot)
178            .await
179    }
180
181    /// List snapshots stored in the local blob store.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if snapshot manifests cannot be read.
186    pub async fn list_snapshots(&self) -> Result<Vec<Snapshot>> {
187        SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
188            .list()
189            .await
190    }
191
192    /// Delete a snapshot and release its pins.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the snapshot cannot be found or pins cannot be released.
197    pub async fn delete_snapshot(&self, snapshot_id: SnapshotId) -> Result<()> {
198        SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
199            .delete(snapshot_id)
200            .await
201    }
202
203    /// Compare two snapshots.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if either snapshot is invalid.
208    pub fn diff_snapshots(&self, first: &Snapshot, second: &Snapshot) -> Result<SnapshotDiff> {
209        first.diff(second)
210    }
211
212    /// # Errors
213    ///
214    /// Returns an error if the blob fails to be stored or set.
215    pub async fn set_blob(&self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Hash> {
216        if !self.sync_mode.can_write() {
217            return Err(SyncwebError::WriteDenied {
218                mode: self.sync_mode.to_string(),
219            });
220        }
221        let value_bytes = value.as_ref();
222        let hash = self.blob_store.add_bytes(value_bytes).await?;
223        let len = u64::try_from(value_bytes.len())
224            .map_err(|error| SyncwebError::operation("blob size exceeds u64::MAX", error))?;
225        self.docs_engine
226            .set_blob(&self.doc, self.author, key, hash, len)
227            .await?;
228        Ok(hash)
229    }
230
231    /// Store an existing blob reference in this folder.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if the folder is read-only, the blob is unavailable, or
236    /// the document entry cannot be written.
237    pub async fn set_blob_ref(&self, key: impl AsRef<[u8]>, hash: Hash, size: u64) -> Result<()> {
238        if !self.sync_mode.can_write() {
239            return Err(SyncwebError::WriteDenied {
240                mode: self.sync_mode.to_string(),
241            });
242        }
243        if !self.blob_store.has(hash).await? {
244            return Err(SyncwebError::InvalidConfig(format!("blob is missing: {hash}")));
245        }
246        self.docs_engine.set_blob(&self.doc, self.author, key, hash, size).await
247    }
248
249    /// Delete a folder entry.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if the folder is read-only or the document entry cannot be deleted.
254    pub async fn delete_entry(&self, key: impl AsRef<[u8]>) -> Result<()> {
255        if !self.sync_mode.can_write() {
256            return Err(SyncwebError::WriteDenied {
257                mode: self.sync_mode.to_string(),
258            });
259        }
260        self.docs_engine.delete(&self.doc, self.author, key).await
261    }
262
263    /// # Errors
264    ///
265    /// Returns an error if the folder ticket cannot be created.
266    pub async fn ticket(&self, endpoint: iroh::EndpointAddr, writable: bool) -> Result<DocTicket> {
267        let mode = if writable && self.sync_mode.can_write() {
268            ShareMode::Write
269        } else {
270            ShareMode::Read
271        };
272        self.docs_engine.share(&self.doc, mode, endpoint).await
273    }
274
275    /// Create an unauthenticated ticket for a blob in this folder and pin it
276    /// while it is publicly shared.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the blob is unavailable or cannot be pinned.
281    pub async fn publish_blob(&self, endpoint: iroh::EndpointAddr, hash: Hash) -> Result<BlobTicket> {
282        if !self.blob_store.has(hash).await? {
283            return Err(SyncwebError::InvalidConfig(format!(
284                "cannot publish missing blob {hash}"
285            )));
286        }
287        self.blob_store
288            .pin(public_pin_name(self.namespace_id, hash), hash)
289            .await?;
290        Ok(self.blob_store.ticket_for_addr(endpoint, hash))
291    }
292
293    /// Remove the public-sharing pin from a folder blob.
294    ///
295    /// Existing blob tickets are capabilities and remain usable while another
296    /// tag or active transfer retains the blob.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the public-sharing pin cannot be removed.
301    pub async fn unpublish_blob(&self, hash: Hash) -> Result<()> {
302        self.blob_store.unpin(public_pin_name(self.namespace_id, hash)).await
303    }
304}
305
306fn public_pin_name(namespace_id: NamespaceId, hash: Hash) -> String {
307    format!("syncweb/public/{namespace_id}/{hash}")
308}