syncweb_core/folder/
manager.rs1use 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 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 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 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 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 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 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 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 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}