1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18pub struct ContainerManifestV1 {
19 pub v: u8,
20 pub object: Key,
21 pub fec: Option<FecParams>,
22 pub assets: Vec<Key>,
23 pub sealed_meta: Option<Key>,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct FecParams {
28 pub k: usize,
29 pub m: usize,
30 pub shard_size: usize,
31}
32
33#[allow(dead_code)]
35async fn container_manifest_put(
36 manifest: &ContainerManifestV1,
37 _fec: &FecParams,
38 _policy: &PutPolicy,
39) -> Result<Key> {
40 use blake3::Hasher;
41
42 let manifest_bytes = bincode::serialize(manifest)?;
44
45 let mut hasher = Hasher::new();
47 hasher.update(&manifest_bytes);
48 let key_bytes = hasher.finalize();
49 let key = Key::from(*key_bytes.as_bytes());
50
51 Ok(key)
56}
57
58#[allow(dead_code)]
60async fn container_manifest_fetch(_key: &[u8]) -> Result<ContainerManifestV1> {
61 Ok(ContainerManifestV1 {
64 v: 1,
65 object: Key::new([0u8; 32]),
66 fec: None,
67 assets: Vec::new(),
68 sealed_meta: None,
69 })
70}
71
72use crate::dht::PutPolicy;
73use crate::fwid::Key;
74use anyhow::Result;
75use chrono::{DateTime, Utc};
76use serde::{Deserialize, Serialize};
77use std::collections::HashMap;
78use std::path::PathBuf;
79use std::sync::Arc;
80use tokio::sync::RwLock;
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum DiskType {
85 Private,
87 Public,
89 Shared,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct DiskConfig {
96 pub max_size: u64,
98 pub encrypted: bool,
100 pub fec: FecParams,
102 pub auto_sync_interval: Option<u64>,
104}
105
106impl Default for DiskConfig {
107 fn default() -> Self {
108 Self {
109 max_size: 1_073_741_824, encrypted: true,
111 fec: FecParams {
112 k: 4, m: 2, shard_size: 65536, },
116 auto_sync_interval: Some(300), }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct DiskHandle {
124 pub entity_id: Key,
126 pub disk_type: DiskType,
128 pub config: DiskConfig,
130 pub root_manifest: Key,
132 state: Arc<RwLock<DiskState>>,
134}
135
136#[derive(Debug)]
138struct DiskState {
139 files: HashMap<PathBuf, FileEntry>,
141 used_space: u64,
143 last_sync: DateTime<Utc>,
145 dirty: bool,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct FileEntry {
152 pub path: PathBuf,
154 pub size: u64,
156 pub content_hash: Key,
158 pub metadata: FileMetadata,
160 pub created_at: DateTime<Utc>,
162 pub modified_at: DateTime<Utc>,
164 pub is_directory: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct FileMetadata {
171 pub mime_type: Option<String>,
173 pub attributes: HashMap<String, String>,
175 pub permissions: u32,
177}
178
179impl Default for FileMetadata {
180 fn default() -> Self {
181 Self {
182 mime_type: None,
183 attributes: HashMap::new(),
184 permissions: 0o644,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct WriteReceipt {
192 pub path: PathBuf,
194 pub content_hash: Key,
196 pub bytes_written: u64,
198 pub timestamp: DateTime<Utc>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct SyncStatus {
205 pub files_synced: usize,
207 pub bytes_synced: u64,
209 pub timestamp: DateTime<Utc>,
211 pub errors: Vec<String>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Asset {
218 pub path: String,
220 pub content: Vec<u8>,
222 pub mime_type: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PublishReceipt {
229 pub entity_id: Key,
231 pub website_root: Key,
233 pub manifest_key: Key,
235 pub timestamp: DateTime<Utc>,
237}
238
239pub async fn disk_create(
241 entity_id: Key,
242 disk_type: DiskType,
243 config: DiskConfig,
244) -> Result<DiskHandle> {
245 let mut hash_input = Vec::new();
247 hash_input.extend_from_slice(entity_id.as_bytes());
248 hash_input.push(disk_type as u8);
249 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
250
251 let state = Arc::new(RwLock::new(DiskState {
253 files: HashMap::new(),
254 used_space: 0,
255 last_sync: Utc::now(),
256 dirty: false,
257 }));
258
259 let manifest = ContainerManifestV1 {
261 v: 1,
262 object: root_manifest.clone(),
263 fec: Some(config.fec.clone()),
264 assets: Vec::new(),
265 sealed_meta: if config.encrypted {
266 Some(Key::from([0u8; 32])) } else {
268 None
269 },
270 };
271
272 #[cfg(any(test, feature = "test-utils"))]
274 {
275 use crate::mock_dht::mock_ops;
276 mock_ops::container_manifest_put(
277 &manifest,
278 &PutPolicy {
279 quorum: 3,
280 ttl: None,
281 },
283 )
284 .await?;
285 }
286
287 #[cfg(not(any(test, feature = "test-utils")))]
288 {
289 let fec_params = manifest.fec.as_ref().unwrap_or(&config.fec);
290 container_manifest_put(
291 &manifest,
292 fec_params,
293 &PutPolicy {
294 quorum: 3,
295 ttl: None,
296 },
298 )
299 .await?;
300 }
301
302 Ok(DiskHandle {
303 entity_id,
304 disk_type,
305 config,
306 root_manifest,
307 state,
308 })
309}
310
311pub async fn disk_mount(entity_id: Key, disk_type: DiskType) -> Result<DiskHandle> {
313 let mut hash_input = Vec::new();
315 hash_input.extend_from_slice(entity_id.as_bytes());
316 hash_input.push(disk_type as u8);
317 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
318
319 #[cfg(any(test, feature = "test-utils"))]
321 let _manifest =
322 crate::mock_dht::mock_ops::container_manifest_fetch(root_manifest.as_bytes()).await?;
323
324 #[cfg(not(any(test, feature = "test-utils")))]
325 let _manifest = container_manifest_fetch(root_manifest.as_bytes()).await?;
326
327 let state = Arc::new(RwLock::new(DiskState {
330 files: HashMap::new(),
331 used_space: 0,
332 last_sync: Utc::now(),
333 dirty: false,
334 }));
335
336 Ok(DiskHandle {
337 entity_id,
338 disk_type,
339 config: DiskConfig::default(),
340 root_manifest,
341 state,
342 })
343}
344
345pub async fn disk_write(
347 handle: &DiskHandle,
348 path: &str,
349 content: &[u8],
350 metadata: FileMetadata,
351) -> Result<WriteReceipt> {
352 let path_buf = PathBuf::from(path);
353
354 let mut state = handle.state.write().await;
356 if state.used_space + content.len() as u64 > handle.config.max_size {
357 anyhow::bail!("Disk space exceeded");
358 }
359
360 let content_hash = Key::from(*blake3::hash(content).as_bytes());
362
363 #[cfg(any(test, feature = "test-utils"))]
368 {
369 let stored_content = if handle.config.encrypted {
371 content.to_vec()
373 } else {
374 content.to_vec()
375 };
376 let pol = PutPolicy {
377 quorum: 3,
378 ttl: None,
379 };
381 crate::mock_dht::mock_ops::dht_put(
382 content_hash.clone(),
383 bytes::Bytes::from(stored_content),
384 &pol,
385 )
386 .await?;
387 }
388
389 #[cfg(not(any(test, feature = "test-utils")))]
390 {
391 let _pol = PutPolicy {
392 quorum: 3,
393 ttl: None,
394 };
396 }
404
405 let now = Utc::now();
407 let entry = FileEntry {
408 path: path_buf.clone(),
409 size: content.len() as u64,
410 content_hash: content_hash.clone(),
411 metadata,
412 created_at: state
413 .files
414 .get(&path_buf)
415 .map(|e| e.created_at)
416 .unwrap_or(now),
417 modified_at: now,
418 is_directory: false,
419 };
420
421 if let Some(old_entry) = state.files.insert(path_buf.clone(), entry) {
423 state.used_space -= old_entry.size;
424 }
425 state.used_space += content.len() as u64;
426 state.dirty = true;
427
428 Ok(WriteReceipt {
429 path: path_buf,
430 content_hash,
431 bytes_written: content.len() as u64,
432 timestamp: now,
433 })
434}
435
436pub async fn disk_read(handle: &DiskHandle, path: &str) -> Result<Vec<u8>> {
438 let path_buf = PathBuf::from(path);
439
440 let state = handle.state.read().await;
442 let entry = state
443 .files
444 .get(&path_buf)
445 .ok_or_else(|| anyhow::anyhow!("File not found: {}", path))?;
446
447 if entry.is_directory {
448 anyhow::bail!("Path is a directory: {}", path);
449 }
450
451 #[cfg(any(test, feature = "test-utils"))]
453 let content = crate::mock_dht::mock_ops::dht_get(entry.content_hash.clone(), 1).await?;
454
455 #[cfg(not(any(test, feature = "test-utils")))]
456 let content = bytes::Bytes::from(vec![]);
459
460 let decrypted = if handle.config.encrypted {
462 content.to_vec()
464 } else {
465 content.to_vec()
466 };
467
468 Ok(decrypted)
469}
470
471pub async fn disk_list(handle: &DiskHandle, path: &str, recursive: bool) -> Result<Vec<FileEntry>> {
473 let search_path = if path == "." {
474 PathBuf::new() } else {
476 PathBuf::from(path)
477 };
478 let state = handle.state.read().await;
479
480 let mut results = Vec::new();
481
482 for (file_path, entry) in &state.files {
483 if recursive {
485 if search_path.as_os_str().is_empty() || file_path.starts_with(&search_path) {
487 results.push(entry.clone());
488 }
489 } else {
490 if let Some(parent) = file_path.parent() {
492 if parent == search_path {
493 results.push(entry.clone());
494 }
495 } else if search_path.as_os_str().is_empty() {
496 results.push(entry.clone());
498 }
499 }
500 }
501
502 results.sort_by(|a, b| a.path.cmp(&b.path));
504
505 Ok(results)
506}
507
508pub async fn disk_delete(handle: &DiskHandle, path: &str) -> Result<()> {
510 let path_buf = PathBuf::from(path);
511
512 let mut state = handle.state.write().await;
513
514 if let Some(entry) = state.files.remove(&path_buf) {
516 state.used_space -= entry.size;
517 state.dirty = true;
518 Ok(())
519 } else {
520 Err(anyhow::anyhow!("File not found: {}", path))
521 }
522}
523
524pub async fn disk_sync(handle: &DiskHandle) -> Result<SyncStatus> {
526 let mut state = handle.state.write().await;
527
528 if !state.dirty {
529 return Ok(SyncStatus {
530 files_synced: 0,
531 bytes_synced: 0,
532 timestamp: Utc::now(),
533 errors: Vec::new(),
534 });
535 }
536
537 let file_list: Vec<FileEntry> = state.files.values().cloned().collect();
539 let tree_bytes = serde_cbor::to_vec(&file_list)?;
540 let tree_hash = Key::from(*blake3::hash(&tree_bytes).as_bytes());
541
542 #[cfg(any(test, feature = "test-utils"))]
544 {
545 let pol = PutPolicy {
546 quorum: 3,
547 ttl: None,
548 };
550 crate::mock_dht::mock_ops::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol)
551 .await?;
552
553 let manifest = ContainerManifestV1 {
555 v: 1,
556 object: handle.root_manifest.clone(),
557 fec: Some(handle.config.fec.clone()),
558 assets: vec![tree_hash],
559 sealed_meta: if handle.config.encrypted {
560 Some(Key::from([0u8; 32])) } else {
562 None
563 },
564 };
565
566 crate::mock_dht::mock_ops::container_manifest_put(&manifest, &pol).await?;
567 }
568
569 #[cfg(not(any(test, feature = "test-utils")))]
570 {
571 let pol = PutPolicy {
572 quorum: 3,
573 ttl: None,
574 };
576 let manifest = ContainerManifestV1 {
581 v: 1,
582 object: handle.root_manifest.clone(),
583 fec: Some(handle.config.fec.clone()),
584 assets: vec![tree_hash],
585 sealed_meta: if handle.config.encrypted {
586 Some(Key::from([0u8; 32])) } else {
588 None
589 },
590 };
591
592 let fec_params = manifest.fec.as_ref().unwrap_or(&handle.config.fec);
593 container_manifest_put(&manifest, fec_params, &pol).await?;
594 }
595
596 state.dirty = false;
598 state.last_sync = Utc::now();
599
600 Ok(SyncStatus {
601 files_synced: state.files.len(),
602 bytes_synced: state.used_space,
603 timestamp: state.last_sync,
604 errors: Vec::new(),
605 })
606}
607
608pub async fn website_set_home(
610 handle: &DiskHandle,
611 markdown_content: &str,
612 assets: Vec<Asset>,
613) -> Result<()> {
614 disk_write(
616 handle,
617 "home.md",
618 markdown_content.as_bytes(),
619 FileMetadata {
620 mime_type: Some("text/markdown".to_string()),
621 ..Default::default()
622 },
623 )
624 .await?;
625
626 for asset in assets {
628 disk_write(
629 handle,
630 &asset.path,
631 &asset.content,
632 FileMetadata {
633 mime_type: Some(asset.mime_type),
634 ..Default::default()
635 },
636 )
637 .await?;
638 }
639
640 disk_sync(handle).await?;
642
643 Ok(())
644}
645
646pub async fn website_publish(entity_id: Key, website_root: Key) -> Result<PublishReceipt> {
648 let handle = match disk_mount(entity_id.clone(), DiskType::Public).await {
650 Ok(h) => h,
651 Err(_) => disk_create(entity_id.clone(), DiskType::Public, DiskConfig::default()).await?,
652 };
653
654 disk_sync(&handle).await?;
656
657 Ok(PublishReceipt {
662 entity_id,
663 website_root,
664 manifest_key: handle.root_manifest,
665 timestamp: Utc::now(),
666 })
667}