Skip to main content

syncweb_core/folder/
manager.rs

1use std::{
2    collections::{HashMap, hash_map::Entry},
3    str::FromStr,
4    sync::Arc,
5};
6
7use iroh::PublicKey;
8use iroh_blobs::ticket::BlobTicket;
9use iroh_docs::{DocTicket, NamespaceId, api::Doc};
10use n0_future::StreamExt;
11use tokio::sync::RwLock;
12
13use crate::error::{Result, SyncwebError};
14use crate::node::iroh_node::IrohNode;
15
16use super::{Capability, SyncMode, SyncwebFolder};
17
18const MODE_KEY: &[u8] = b"sys/syncweb/mode";
19
20#[derive(Clone)]
21pub struct FolderManager {
22    endpoint: iroh::Endpoint,
23    endpoint_addr: iroh::EndpointAddr,
24    node_id: PublicKey,
25    blob_store: crate::node::blob_store::BlobStore,
26    docs_engine: crate::node::docs_engine::DocsEngine,
27    folders: Arc<RwLock<HashMap<NamespaceId, SyncwebFolder>>>,
28}
29
30impl FolderManager {
31    #[must_use]
32    pub fn new(node: &IrohNode) -> Self {
33        Self {
34            endpoint: node.endpoint().clone(),
35            endpoint_addr: node.endpoint().addr(),
36            node_id: node.endpoint().id(),
37            blob_store: node.blob_store().clone(),
38            docs_engine: node.docs_engine().clone(),
39            folders: Arc::new(RwLock::new(HashMap::new())),
40        }
41    }
42
43    /// # Errors
44    ///
45    /// Returns an error if the folder namespace cannot be created or initialized.
46    pub async fn create(&self, mode: SyncMode) -> Result<SyncwebFolder> {
47        let doc = self.docs_engine.create_namespace().await?;
48        let author = self.docs_engine.author().await?;
49        self.docs_engine.set(&doc, author, MODE_KEY, mode.to_string()).await?;
50        let folder = SyncwebFolder::new(doc, author, self.blob_store.clone(), self.docs_engine.clone(), mode);
51        folder.grant(self.node_id, Capability::Admin).await;
52        self.folders.write().await.insert(folder.namespace_id(), folder.clone());
53        Ok(folder)
54    }
55
56    /// # Errors
57    ///
58    /// Returns an error if the folder ticket cannot be joined or parsed.
59    pub async fn join(&self, ticket_str: impl AsRef<str>, mode: SyncMode) -> Result<SyncwebFolder> {
60        let mut ticket_raw = ticket_str.as_ref();
61        if let Some(rest) = ticket_raw.strip_prefix("syncweb://") {
62            if let Some((_, query)) = rest.split_once('?') {
63                for param in query.split('&') {
64                    if let Some(val) = param.strip_prefix("ticket=") {
65                        ticket_raw = val;
66                        break;
67                    }
68                }
69            } else {
70                ticket_raw = rest;
71            }
72        }
73        let ticket = DocTicket::from_str(ticket_raw).map_err(|error| SyncwebError::InvalidTicket(error.to_string()))?;
74        let doc = self.docs_engine.import_ticket(ticket).await?;
75        let folder = self.folder_from_doc(doc, mode).await?;
76        self.folders.write().await.insert(folder.namespace_id(), folder.clone());
77        Ok(folder)
78    }
79
80    /// Subscribe to a public blob ticket by creating a local read-only
81    /// namespace containing the fetched content.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the blob cannot be fetched or the local namespace
86    /// cannot be created.
87    pub async fn subscribe_public(&self, ticket: &BlobTicket) -> Result<SyncwebFolder> {
88        self.blob_store.fetch(&self.endpoint, ticket).await?;
89        let bytes = self.blob_store.get(ticket.hash()).await?;
90        let doc = self.docs_engine.create_namespace().await?;
91        let author = self.docs_engine.author().await?;
92        self.docs_engine
93            .set(&doc, author, MODE_KEY, SyncMode::PublicReadOnly.to_string())
94            .await?;
95        self.docs_engine
96            .set_blob(
97                &doc,
98                author,
99                b"public/content",
100                ticket.hash(),
101                u64::try_from(bytes.len())
102                    .map_err(|error| SyncwebError::operation("public blob is too large", error))?,
103            )
104            .await?;
105        let folder = SyncwebFolder::new(
106            doc,
107            author,
108            self.blob_store.clone(),
109            self.docs_engine.clone(),
110            SyncMode::PublicReadOnly,
111        );
112        self.folders.write().await.insert(folder.namespace_id(), folder.clone());
113        Ok(folder)
114    }
115
116    /// # Errors
117    ///
118    /// Returns an error if the folder namespace cannot be accepted.
119    pub async fn accept(&self, namespace_id: NamespaceId) -> Result<SyncwebFolder> {
120        let existing_folder = self.folders.read().await.get(&namespace_id).cloned();
121        if let Some(folder) = existing_folder {
122            return Ok(folder);
123        }
124        let doc = self
125            .docs_engine
126            .open(namespace_id)
127            .await?
128            .ok_or(SyncwebError::NamespaceNotAvailable)?;
129        let folder = self.folder_from_doc(doc, SyncMode::ReceiveOnly).await?;
130        self.folders.write().await.insert(namespace_id, folder.clone());
131        Ok(folder)
132    }
133
134    /// # Errors
135    ///
136    /// Returns an error if the folder namespace cannot be dropped.
137    pub async fn drop(&self, namespace_id: NamespaceId) -> Result<()> {
138        self.docs_engine.drop_namespace(namespace_id).await?;
139        self.folders.write().await.remove(&namespace_id);
140        Ok(())
141    }
142
143    /// # Errors
144    ///
145    /// Returns an error if the folders cannot be listed.
146    pub async fn list(&self) -> Result<Vec<SyncwebFolder>> {
147        let mut documents = self
148            .docs_engine
149            .inner()
150            .list()
151            .await
152            .map_err(|error| SyncwebError::operation("failed to list documents", error))?;
153        let mut listed = Vec::new();
154        while let Some(document) = documents.next().await {
155            listed.push(document.map_err(|error| SyncwebError::operation("failed to read document list", error))?);
156        }
157        for (namespace_id, capability) in listed {
158            if self.folders.read().await.contains_key(&namespace_id) {
159                continue;
160            }
161            let doc = self
162                .docs_engine
163                .open(namespace_id)
164                .await?
165                .ok_or(SyncwebError::NamespaceNotAvailable)?;
166            let fallback_mode = match capability {
167                iroh_docs::CapabilityKind::Write => SyncMode::SendReceive,
168                iroh_docs::CapabilityKind::Read => SyncMode::ReceiveOnly,
169            };
170            let mode = self.mode_from_doc(&doc, fallback_mode).await?;
171            let folder = self.folder_from_doc(doc, mode).await?;
172            if let Entry::Vacant(entry) = self.folders.write().await.entry(namespace_id) {
173                entry.insert(folder);
174            }
175        }
176        Ok(self.folders.read().await.values().cloned().collect())
177    }
178
179    /// Return a managed folder, loading locally available namespaces first.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the local namespace list cannot be read.
184    pub async fn get(&self, namespace_id: NamespaceId) -> Result<SyncwebFolder> {
185        let existing = self.folders.read().await.get(&namespace_id).cloned();
186        if let Some(folder) = existing {
187            return Ok(folder);
188        }
189        self.list()
190            .await?
191            .into_iter()
192            .find(|folder| folder.namespace_id() == namespace_id)
193            .ok_or_else(|| SyncwebError::FolderNotFound(namespace_id.to_string()))
194    }
195
196    /// # Errors
197    ///
198    /// Returns an error if the ticket cannot be generated.
199    pub async fn ticket(&self, namespace_id: NamespaceId, writable: bool) -> Result<DocTicket> {
200        let folder = self
201            .folders
202            .read()
203            .await
204            .get(&namespace_id)
205            .cloned()
206            .ok_or(SyncwebError::FolderNotFound(namespace_id.to_string()))?;
207        folder.ticket(self.endpoint_addr.clone(), writable).await
208    }
209
210    async fn folder_from_doc(&self, doc: Doc, mode: SyncMode) -> Result<SyncwebFolder> {
211        Ok(SyncwebFolder::new(
212            doc,
213            self.docs_engine.author().await?,
214            self.blob_store.clone(),
215            self.docs_engine.clone(),
216            mode,
217        ))
218    }
219
220    async fn mode_from_doc(&self, doc: &Doc, fallback: SyncMode) -> Result<SyncMode> {
221        let author = self.docs_engine.author().await?;
222        let Some(entry) = self.docs_engine.get(doc, author, MODE_KEY).await? else {
223            return Ok(fallback);
224        };
225        let mode_bytes = self.blob_store.get(entry.content_hash()).await?;
226        let mode_value = std::str::from_utf8(&mode_bytes)
227            .map_err(|error| SyncwebError::operation("folder mode metadata is not UTF-8", error))?;
228        mode_value.parse()
229    }
230}