1use anyhow::{Result, anyhow};
23use chacha20poly1305::{
24 ChaCha20Poly1305, Nonce,
25 aead::{Aead, KeyInit},
26};
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30use tokio::sync::RwLock;
31use tracing::{debug, info};
32use yrs::{
33 Doc, GetString, ReadTxn, StateVector, Text, Transact, Update,
34 updates::decoder::Decode,
35 updates::encoder::{Encoder, EncoderV1},
36};
37
38pub type DocumentId = String;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum StorageMode {
44 Files,
46 Web,
48 Both,
50}
51
52#[derive(Clone)]
54pub struct DocReplicatorConfig {
55 pub files_storage_enabled: bool,
56 pub web_storage_enabled: bool,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61struct DocumentMetadata {
62 id: DocumentId,
63 name: String,
64 storage_mode: StorageMode,
65 created_at: chrono::DateTime<chrono::Utc>,
66 updated_at: chrono::DateTime<chrono::Utc>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71struct EncryptedDocument {
72 nonce: [u8; 12],
73 ciphertext: Vec<u8>,
74}
75
76pub struct DocReplicator {
78 documents: Arc<RwLock<HashMap<DocumentId, Arc<Mutex<Doc>>>>>,
80
81 metadata: Arc<RwLock<HashMap<DocumentId, DocumentMetadata>>>,
83
84 files_storage: Arc<RwLock<HashMap<DocumentId, EncryptedDocument>>>,
86 encryption_keys: Arc<RwLock<HashMap<DocumentId, [u8; 32]>>>,
87
88 web_storage: Arc<RwLock<HashMap<DocumentId, Vec<u8>>>>,
90
91 files_enabled: bool,
93 web_enabled: bool,
94}
95
96impl DocReplicator {
97 pub async fn new(config: DocReplicatorConfig) -> Result<Self> {
99 info!("Creating DocReplicator");
100
101 Ok(Self {
102 documents: Arc::new(RwLock::new(HashMap::new())),
103 metadata: Arc::new(RwLock::new(HashMap::new())),
104 files_storage: Arc::new(RwLock::new(HashMap::new())),
105 encryption_keys: Arc::new(RwLock::new(HashMap::new())),
106 web_storage: Arc::new(RwLock::new(HashMap::new())),
107 files_enabled: config.files_storage_enabled,
108 web_enabled: config.web_storage_enabled,
109 })
110 }
111
112 pub async fn create_document(
114 &self,
115 name: &str,
116 storage_mode: StorageMode,
117 ) -> Result<DocumentId> {
118 let mut default_key = [0u8; 32];
120 getrandom::getrandom(&mut default_key)
121 .map_err(|e| anyhow!("Failed to generate key: {}", e))?;
122
123 self.create_document_with_key(name, storage_mode, &default_key)
124 .await
125 }
126
127 pub async fn create_document_with_key(
132 &self,
133 name: &str,
134 storage_mode: StorageMode,
135 encryption_key: &[u8; 32],
136 ) -> Result<DocumentId> {
137 let doc_id = if name.contains('-') || name.len() > 30 {
140 name.to_string()
142 } else {
143 uuid::Uuid::new_v4().to_string()
145 };
146
147 let now = chrono::Utc::now();
148
149 debug!(
150 "Creating document '{}' with ID {} and mode {:?}",
151 name, doc_id, storage_mode
152 );
153
154 debug!("Step 1: Creating Yrs Doc");
155 let doc = Doc::new();
157 let doc = Arc::new(Mutex::new(doc));
158 debug!("Step 1: Done creating Yrs Doc");
159
160 debug!("Step 2: Storing document in documents map");
161 self.documents.write().await.insert(doc_id.clone(), doc);
163 debug!("Step 2: Done storing document");
164
165 debug!("Step 3: Storing metadata");
166 let meta = DocumentMetadata {
168 id: doc_id.clone(),
169 name: name.to_string(),
170 storage_mode,
171 created_at: now,
172 updated_at: now,
173 };
174 self.metadata.write().await.insert(doc_id.clone(), meta);
175 debug!("Step 3: Done storing metadata");
176
177 debug!("Step 4: Storing encryption key");
178 if storage_mode == StorageMode::Files || storage_mode == StorageMode::Both {
180 self.encryption_keys
181 .write()
182 .await
183 .insert(doc_id.clone(), *encryption_key);
184 }
185 debug!("Step 4: Done storing encryption key");
186
187 debug!("Saving document to storage");
189 self.update_storage(&doc_id).await?;
190 debug!("Done saving document");
191
192 info!("Document '{}' created with ID {}", name, doc_id);
193
194 Ok(doc_id)
195 }
196
197 pub async fn get_document(&self, doc_id: &str) -> Result<Option<Arc<Mutex<Doc>>>> {
199 Ok(self.documents.read().await.get(doc_id).cloned())
200 }
201
202 pub async fn document_exists_in_files(&self, doc_id: &str) -> Result<bool> {
204 Ok(self.files_storage.read().await.contains_key(doc_id))
205 }
206
207 pub async fn document_exists_in_web(&self, doc_id: &str) -> Result<bool> {
209 Ok(self.web_storage.read().await.contains_key(doc_id))
210 }
211
212 pub async fn insert_text(&self, doc_id: &str, index: usize, text: &str) -> Result<()> {
214 let doc = self
215 .get_document(doc_id)
216 .await?
217 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
218
219 let doc_clone = Arc::clone(&doc);
221 let text_owned = text.to_string();
222
223 tokio::task::spawn_blocking(move || {
225 let doc = doc_clone
226 .lock()
227 .map_err(|e| anyhow!("Mutex lock failed: {}", e))?;
228
229 let ytext = doc.get_or_insert_text("content");
231 let mut txn = doc.transact_mut();
232
233 ytext.insert(&mut txn, index as u32, &text_owned);
234
235 drop(txn);
236 Ok::<(), anyhow::Error>(())
237 })
238 .await
239 .map_err(|e| anyhow!("Join error: {}", e))??;
240
241 self.update_storage(doc_id).await?;
243
244 debug!("Inserted '{}' at position {} in {}", text, index, doc_id);
245
246 Ok(())
247 }
248
249 pub async fn delete_text(&self, doc_id: &str, index: usize, len: usize) -> Result<()> {
251 let doc = self
252 .get_document(doc_id)
253 .await?
254 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
255
256 let doc_clone = Arc::clone(&doc);
258
259 let actual_len = tokio::task::spawn_blocking(move || {
261 let doc = doc_clone
262 .lock()
263 .map_err(|e| anyhow!("Mutex lock failed: {}", e))?;
264
265 let ytext = doc.get_or_insert_text("content");
267 let mut txn = doc.transact_mut();
268
269 let current_len = ytext.len(&txn);
271
272 let actual_len = if index + len > current_len as usize {
274 if index >= current_len as usize {
275 return Ok(0); }
277 current_len as usize - index
278 } else {
279 len
280 };
281
282 ytext.remove_range(&mut txn, index as u32, actual_len as u32);
283
284 drop(txn);
285 Ok::<usize, anyhow::Error>(actual_len)
286 })
287 .await
288 .map_err(|e| anyhow!("Join error: {}", e))??;
289
290 self.update_storage(doc_id).await?;
292
293 debug!(
294 "Deleted {} chars at position {} in {}",
295 actual_len, index, doc_id
296 );
297
298 Ok(())
299 }
300
301 pub async fn get_text(&self, doc_id: &str) -> Result<String> {
303 let doc = self
304 .get_document(doc_id)
305 .await?
306 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
307
308 let doc_clone = Arc::clone(&doc);
310
311 let text = tokio::task::spawn_blocking(move || {
313 let doc = doc_clone
314 .lock()
315 .map_err(|e| anyhow!("Mutex lock failed: {}", e))?;
316
317 let ytext = doc.get_or_insert_text("content");
319 let txn = doc.transact();
320 let text = ytext.get_string(&txn);
321
322 Ok::<String, anyhow::Error>(text)
323 })
324 .await
325 .map_err(|e| anyhow!("Join error: {}", e))??;
326
327 Ok(text)
328 }
329
330 pub async fn get_crdt_update(&self, doc_id: &str) -> Result<Vec<u8>> {
335 let doc = self
336 .get_document(doc_id)
337 .await?
338 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
339
340 let doc_clone = Arc::clone(&doc);
342
343 let update = tokio::task::spawn_blocking(move || {
345 let doc = doc_clone
346 .lock()
347 .map_err(|e| anyhow!("Mutex lock failed: {}", e))?;
348 let txn = doc.transact();
349
350 let empty_state = StateVector::default();
353
354 let mut encoder = EncoderV1::new();
356 txn.encode_state_as_update(&empty_state, &mut encoder);
357
358 Ok::<Vec<u8>, anyhow::Error>(encoder.to_vec())
359 })
360 .await
361 .map_err(|e| anyhow!("Join error: {}", e))??;
362
363 Ok(update)
364 }
365
366 pub async fn apply_crdt_update(&self, doc_id: &str, update: &[u8]) -> Result<()> {
368 if self.get_document(doc_id).await?.is_none() {
370 self.create_document_with_id(doc_id, StorageMode::Files)
371 .await?;
372 }
373
374 let doc = self
375 .get_document(doc_id)
376 .await?
377 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
378
379 let doc_clone = Arc::clone(&doc);
381 let update_owned = update.to_vec();
382
383 tokio::task::spawn_blocking(move || {
385 let doc = doc_clone
386 .lock()
387 .map_err(|e| anyhow!("Mutex lock failed: {}", e))?;
388 let mut txn = doc.transact_mut();
389
390 let update_obj = Update::decode_v1(&update_owned)
391 .map_err(|e| anyhow!("Failed to decode update: {:?}", e))?;
392
393 txn.apply_update(update_obj);
394
395 drop(txn);
396 Ok::<(), anyhow::Error>(())
397 })
398 .await
399 .map_err(|e| anyhow!("Join error: {}", e))??;
400
401 self.update_storage(doc_id).await?;
403
404 debug!("Applied CRDT update to {}", doc_id);
405
406 Ok(())
407 }
408
409 async fn create_document_with_id(&self, doc_id: &str, storage_mode: StorageMode) -> Result<()> {
411 let now = chrono::Utc::now();
412
413 let mut default_key = [0u8; 32];
415 getrandom::getrandom(&mut default_key)
416 .map_err(|e| anyhow!("Failed to generate key: {}", e))?;
417
418 debug!(
419 "Creating document with ID '{}' and mode {:?}",
420 doc_id, storage_mode
421 );
422
423 let doc = Doc::new();
425 let doc = Arc::new(Mutex::new(doc));
426
427 self.documents.write().await.insert(doc_id.to_string(), doc);
429
430 let meta = DocumentMetadata {
432 id: doc_id.to_string(),
433 name: doc_id.to_string(),
434 storage_mode,
435 created_at: now,
436 updated_at: now,
437 };
438 self.metadata.write().await.insert(doc_id.to_string(), meta);
439
440 if storage_mode == StorageMode::Files || storage_mode == StorageMode::Both {
442 self.encryption_keys
443 .write()
444 .await
445 .insert(doc_id.to_string(), default_key);
446 }
447
448 match storage_mode {
450 StorageMode::Files => {
451 self.save_to_files(doc_id).await?;
452 }
453 StorageMode::Web => {
454 self.save_to_web(doc_id).await?;
455 }
456 StorageMode::Both => {
457 self.save_to_files(doc_id).await?;
458 self.save_to_web(doc_id).await?;
459 }
460 }
461
462 info!("Document created with ID {}", doc_id);
463
464 Ok(())
465 }
466
467 pub async fn get_files_blob(&self, doc_id: &str) -> Result<Option<Vec<u8>>> {
469 let storage = self.files_storage.read().await;
470
471 let encrypted = match storage.get(doc_id) {
472 Some(enc) => enc,
473 None => return Ok(None),
474 };
475
476 let blob =
477 bincode::serialize(encrypted).map_err(|e| anyhow!("Serialization failed: {}", e))?;
478
479 Ok(Some(blob))
480 }
481
482 pub async fn get_web_blob(&self, doc_id: &str) -> Result<Option<Vec<u8>>> {
484 Ok(self.web_storage.read().await.get(doc_id).cloned())
485 }
486
487 pub async fn get_encryption_key(&self, doc_id: &str) -> Result<[u8; 32]> {
489 let meta = self
490 .metadata
491 .read()
492 .await
493 .get(doc_id)
494 .cloned()
495 .ok_or_else(|| anyhow!("Document not found: {}", doc_id))?;
496
497 if meta.storage_mode == StorageMode::Web {
498 return Err(anyhow!("Web documents do not have encryption keys"));
499 }
500
501 self.encryption_keys
502 .read()
503 .await
504 .get(doc_id)
505 .copied()
506 .ok_or_else(|| anyhow!("Encryption key not found"))
507 }
508
509 pub async fn decrypt_with_key(&self, doc_id: &str, key: &[u8; 32]) -> Result<Vec<u8>> {
511 let storage = self.files_storage.read().await;
512
513 let encrypted = storage
514 .get(doc_id)
515 .ok_or_else(|| anyhow!("Document not in Files storage"))?;
516
517 let cipher = ChaCha20Poly1305::new(key.into());
518 let nonce = Nonce::from(
519 *<&[u8; 12]>::try_from(encrypted.nonce.as_slice())
520 .map_err(|_| anyhow!("Invalid nonce length"))?,
521 );
522
523 let plaintext = cipher
524 .decrypt(&nonce, encrypted.ciphertext.as_ref())
525 .map_err(|e| anyhow!("Decryption failed: {}", e))?;
526
527 Ok(plaintext)
528 }
529
530 async fn save_to_files(&self, doc_id: &str) -> Result<()> {
532 if !self.files_enabled {
533 return Ok(());
534 }
535
536 let update = self.get_crdt_update(doc_id).await?;
537
538 let key = self
539 .encryption_keys
540 .read()
541 .await
542 .get(doc_id)
543 .copied()
544 .ok_or_else(|| anyhow!("Encryption key not found"))?;
545
546 let mut nonce_bytes = [0u8; 12];
548 getrandom::getrandom(&mut nonce_bytes)
549 .map_err(|e| anyhow!("Nonce generation failed: {}", e))?;
550
551 let cipher = ChaCha20Poly1305::new(&key.into());
552 let nonce = Nonce::from(nonce_bytes);
553
554 let ciphertext = cipher
555 .encrypt(&nonce, update.as_ref())
556 .map_err(|e| anyhow!("Encryption failed: {}", e))?;
557
558 let encrypted = EncryptedDocument {
559 nonce: nonce_bytes,
560 ciphertext,
561 };
562
563 self.files_storage
564 .write()
565 .await
566 .insert(doc_id.to_string(), encrypted);
567
568 debug!("Saved {} to Files storage (encrypted)", doc_id);
569
570 Ok(())
571 }
572
573 async fn save_to_web(&self, doc_id: &str) -> Result<()> {
575 if !self.web_enabled {
576 return Ok(());
577 }
578
579 let update = self.get_crdt_update(doc_id).await?;
580
581 self.web_storage
582 .write()
583 .await
584 .insert(doc_id.to_string(), update);
585
586 debug!("Saved {} to Web storage (public)", doc_id);
587
588 Ok(())
589 }
590
591 async fn update_storage(&self, doc_id: &str) -> Result<()> {
593 let meta = self
594 .metadata
595 .read()
596 .await
597 .get(doc_id)
598 .cloned()
599 .ok_or_else(|| anyhow!("Document metadata not found"))?;
600
601 match meta.storage_mode {
602 StorageMode::Files => self.save_to_files(doc_id).await?,
603 StorageMode::Web => self.save_to_web(doc_id).await?,
604 StorageMode::Both => {
605 self.save_to_files(doc_id).await?;
606 self.save_to_web(doc_id).await?;
607 }
608 }
609
610 self.metadata
612 .write()
613 .await
614 .entry(doc_id.to_string())
615 .and_modify(|m| {
616 m.updated_at = chrono::Utc::now();
617 });
618
619 Ok(())
620 }
621
622 pub async fn list_documents(&self) -> Result<Vec<String>> {
624 debug!("list_documents: About to acquire read lock");
625 let documents = self.documents.read().await;
626 debug!(
627 "list_documents: Acquired read lock, found {} documents",
628 documents.len()
629 );
630 let keys: Vec<String> = documents.keys().cloned().collect();
631 debug!("list_documents: Collected keys");
632 Ok(keys)
633 }
634
635 pub async fn delete_document(&self, doc_id: &str) -> Result<()> {
637 debug!("Deleting document: {}", doc_id);
638
639 self.documents.write().await.remove(doc_id);
641 self.metadata.write().await.remove(doc_id);
642 self.encryption_keys.write().await.remove(doc_id);
643 self.files_storage.write().await.remove(doc_id);
644 self.web_storage.write().await.remove(doc_id);
645
646 info!("Document deleted: {}", doc_id);
647
648 Ok(())
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 async fn create_test_config() -> DocReplicatorConfig {
657 DocReplicatorConfig {
658 files_storage_enabled: true,
659 web_storage_enabled: true,
660 }
661 }
662
663 #[tokio::test]
664 async fn test_doc_replicator_creation() {
665 let config = create_test_config().await;
666 let replicator = DocReplicator::new(config).await.expect("create replicator");
667
668 assert!(replicator.files_enabled);
669 assert!(replicator.web_enabled);
670 }
671
672 #[tokio::test]
673 async fn test_create_and_retrieve_document() {
674 let config = create_test_config().await;
675 let replicator = DocReplicator::new(config).await.expect("create replicator");
676
677 let doc_id = replicator
678 .create_document("test", StorageMode::Files)
679 .await
680 .expect("create doc");
681
682 let doc = replicator.get_document(&doc_id).await.expect("get doc");
683
684 assert!(doc.is_some());
685 }
686
687 #[tokio::test]
688 async fn test_insert_and_get_text() {
689 let config = create_test_config().await;
690 let replicator = DocReplicator::new(config).await.expect("create replicator");
691
692 let doc_id = replicator
693 .create_document("text-test", StorageMode::Files)
694 .await
695 .expect("create");
696
697 replicator
698 .insert_text(&doc_id, 0, "Hello!")
699 .await
700 .expect("insert");
701
702 let text = replicator.get_text(&doc_id).await.expect("get text");
703
704 assert_eq!(text, "Hello!");
705 }
706
707 #[tokio::test]
708 async fn test_dual_storage() {
709 let config = create_test_config().await;
710 let replicator = DocReplicator::new(config).await.expect("create replicator");
711
712 let doc_id = replicator
713 .create_document("dual-doc", StorageMode::Both)
714 .await
715 .expect("create");
716
717 let files_exists = replicator
718 .document_exists_in_files(&doc_id)
719 .await
720 .expect("check files");
721 let web_exists = replicator
722 .document_exists_in_web(&doc_id)
723 .await
724 .expect("check web");
725
726 assert!(files_exists);
727 assert!(web_exists);
728 }
729}