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 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 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 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 pub async fn has_local(&self, hash: Hash) -> Result<bool> {
154 self.blob_store.has(hash).await
155 }
156
157 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 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 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 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 pub fn diff_snapshots(&self, first: &Snapshot, second: &Snapshot) -> Result<SnapshotDiff> {
209 first.diff(second)
210 }
211
212 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 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 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 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 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 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}