1use 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::{
7 AuthorId, DocTicket, NamespaceId,
8 api::{Doc, protocol::ShareMode},
9};
10use tokio::sync::RwLock;
11
12use crate::error::{Result, SyncwebError};
13use crate::node::{blob_store::BlobStore, docs_engine::DocsEngine};
14use crate::snapshot::{Snapshot, SnapshotDiff, SnapshotId, SnapshotStore};
15
16use super::SyncMode;
17use super::public_subscription::{EntryLike, FolderLike};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum Capability {
22 Admin,
23 Write,
24 Read,
25}
26
27impl Capability {
28 #[must_use]
29 pub const fn can_write(self) -> bool {
30 matches!(self, Self::Admin | Self::Write)
31 }
32}
33
34#[derive(Clone)]
35pub struct SyncwebFolder {
36 doc: Doc,
37 namespace_id: NamespaceId,
38 author: AuthorId,
39 blob_store: BlobStore,
40 docs_engine: DocsEngine,
41 sync_mode: SyncMode,
42 capabilities: Arc<RwLock<HashMap<PublicKey, Capability>>>,
43}
44
45impl SyncwebFolder {
46 #[must_use]
47 pub fn new(
48 doc: Doc,
49 author: AuthorId,
50 blob_store: BlobStore,
51 docs_engine: DocsEngine,
52 sync_mode: SyncMode,
53 ) -> Self {
54 let namespace_id = doc.id();
55 Self {
56 doc,
57 namespace_id,
58 author,
59 blob_store,
60 docs_engine,
61 sync_mode,
62 capabilities: Arc::new(RwLock::new(HashMap::new())),
63 }
64 }
65
66 pub async fn create(docs_engine: DocsEngine, blob_store: BlobStore, sync_mode: SyncMode) -> Result<Self> {
72 let doc = docs_engine
73 .create_namespace()
74 .await
75 .map_err(|error| SyncwebError::operation("failed to create folder namespace", error))?;
76 let author = docs_engine
77 .author()
78 .await
79 .map_err(|error| SyncwebError::operation("failed to retrieve folder author", error))?;
80 Ok(Self::new(doc, author, blob_store, docs_engine, sync_mode))
81 }
82
83 pub async fn accept(docs_engine: DocsEngine, blob_store: BlobStore, namespace_id: NamespaceId) -> Result<Self> {
89 let doc = docs_engine
90 .open(namespace_id)
91 .await
92 .map_err(|error| SyncwebError::operation("failed to open folder namespace", error))?
93 .ok_or(SyncwebError::NamespaceNotAvailable)?;
94 let author = docs_engine
95 .author()
96 .await
97 .map_err(|error| SyncwebError::operation("failed to retrieve folder author", error))?;
98 Ok(Self::new(doc, author, blob_store, docs_engine, SyncMode::ReceiveOnly))
99 }
100
101 pub async fn drop_namespace(&self) -> Result<()> {
107 self.docs_engine
108 .drop_namespace(self.namespace_id)
109 .await
110 .map_err(|error| SyncwebError::operation("failed to drop folder namespace", error))
111 }
112
113 #[must_use]
114 pub const fn namespace_id(&self) -> NamespaceId {
115 self.namespace_id
116 }
117
118 #[must_use]
119 pub const fn author(&self) -> AuthorId {
120 self.author
121 }
122
123 #[must_use]
124 pub const fn mode(&self) -> SyncMode {
125 self.sync_mode
126 }
127
128 #[must_use]
129 pub const fn doc(&self) -> &Doc {
130 &self.doc
131 }
132
133 #[must_use]
134 pub const fn docs_engine(&self) -> &DocsEngine {
135 &self.docs_engine
136 }
137
138 pub async fn grant(&self, node_id: PublicKey, capability: Capability) {
139 self.capabilities.write().await.insert(node_id, capability);
140 }
141
142 pub async fn capability(&self, node_id: PublicKey) -> Option<Capability> {
143 self.capabilities.read().await.get(&node_id).copied()
144 }
145
146 pub async fn can_write_as(&self, node_id: PublicKey) -> bool {
147 self.sync_mode.can_write_locally() && self.capability(node_id).await.is_some_and(Capability::can_write)
148 }
149
150 pub async fn has_local(&self, hash: Hash) -> Result<bool> {
156 self.blob_store.has(hash).await
157 }
158
159 pub async fn create_snapshot(&self, description: Option<String>) -> Result<Snapshot> {
166 SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
167 .create_for_folder(self, description)
168 .await
169 }
170
171 pub async fn restore_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
178 SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
179 .restore_for_folder(self, snapshot)
180 .await
181 }
182
183 pub async fn list_snapshots(&self) -> Result<Vec<Snapshot>> {
189 SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
190 .list()
191 .await
192 }
193
194 pub async fn delete_snapshot(&self, snapshot_id: SnapshotId) -> Result<()> {
200 SnapshotStore::with_docs(self.blob_store.clone(), self.docs_engine.clone())
201 .delete(snapshot_id)
202 .await
203 }
204
205 pub fn diff_snapshots(&self, first: &Snapshot, second: &Snapshot) -> Result<SnapshotDiff> {
211 first.diff(second)
212 }
213
214 pub async fn set_blob(&self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Hash> {
218 if !self.sync_mode.can_write_locally() {
219 return Err(SyncwebError::WriteDenied {
220 mode: self.sync_mode.to_string(),
221 });
222 }
223 let value_bytes = value.as_ref();
224 let hash = self.blob_store.add_bytes(value_bytes).await?;
225 let len = u64::try_from(value_bytes.len())
226 .map_err(|error| SyncwebError::operation("blob size exceeds u64::MAX", error))?;
227 self.docs_engine
228 .set_blob(&self.doc, self.author, key, hash, len)
229 .await?;
230 Ok(hash)
231 }
232
233 pub async fn set_blob_ref(&self, key: impl AsRef<[u8]>, hash: Hash, size: u64) -> Result<()> {
240 if !self.sync_mode.can_write_locally() {
241 return Err(SyncwebError::WriteDenied {
242 mode: self.sync_mode.to_string(),
243 });
244 }
245 self.docs_engine.set_blob(&self.doc, self.author, key, hash, size).await
246 }
247
248 pub async fn delete_entry(&self, key: impl AsRef<[u8]>) -> Result<()> {
254 if !self.sync_mode.can_write_locally() {
255 return Err(SyncwebError::WriteDenied {
256 mode: self.sync_mode.to_string(),
257 });
258 }
259 self.docs_engine.delete(&self.doc, self.author, key).await
260 }
261
262 pub async fn ticket(&self, endpoint: iroh::EndpointAddr, writable: bool) -> Result<DocTicket> {
266 let mode = if writable && self.sync_mode.can_grant_write() {
267 ShareMode::Write
268 } else {
269 ShareMode::Read
270 };
271 self.docs_engine.share(&self.doc, mode, endpoint).await
272 }
273
274 pub async fn publish_blob(&self, endpoint: iroh::EndpointAddr, hash: Hash) -> Result<BlobTicket> {
281 if !self.blob_store.has(hash).await? {
282 return Err(SyncwebError::InvalidConfig(format!(
283 "cannot publish missing blob {hash}"
284 )));
285 }
286 let ticket = self.blob_store.ticket_for_addr(endpoint, hash);
287 self.blob_store
288 .pin(public_pin_name(self.namespace_id, hash), hash)
289 .await?;
290 Ok(ticket)
291 }
292
293 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
306#[async_trait]
307impl FolderLike for SyncwebFolder {
308 fn namespace_id(&self) -> String {
309 self.namespace_id.to_string()
310 }
311
312 fn label(&self) -> String {
313 self.namespace_id.to_string()
314 }
315
316 fn kind(&self) -> &'static str {
317 "folder"
318 }
319
320 fn path(&self) -> Option<&Path> {
321 None
322 }
323
324 async fn list_entries(&self) -> Result<Vec<EntryLike>> {
325 let entries = self.docs_engine.list_latest(&self.doc).await?;
326 let mut result = Vec::with_capacity(entries.len());
327 for entry in entries {
328 if entry.key().starts_with(b"sys/") {
329 continue;
330 }
331 let path = String::from_utf8_lossy(entry.key()).to_string();
332 result.push(EntryLike {
333 path,
334 hash: entry.content_hash(),
335 size: entry.content_len(),
336 });
337 }
338 Ok(result)
339 }
340}
341
342fn public_pin_name(namespace_id: NamespaceId, hash: Hash) -> String {
343 format!("syncweb/public/{namespace_id}/{hash}")
344}