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
33async fn container_manifest_put(
35 manifest: &ContainerManifestV1,
36 _fec: &FecParams,
37 _policy: &PutPolicy,
38) -> Result<Key> {
39 use blake3::Hasher;
40
41 let manifest_bytes = bincode::serialize(manifest)?;
43
44 let mut hasher = Hasher::new();
46 hasher.update(&manifest_bytes);
47 let key_bytes = hasher.finalize();
48 let key = Key::from(*key_bytes.as_bytes());
49
50 Ok(key)
55}
56
57async fn container_manifest_fetch(_key: &[u8]) -> Result<ContainerManifestV1> {
59 Ok(ContainerManifestV1 {
62 v: 1,
63 object: Key::new([0u8; 32]),
64 fec: None,
65 assets: Vec::new(),
66 sealed_meta: None,
67 })
68}
69
70use crate::dht::PutPolicy;
71use crate::fwid::Key;
72use anyhow::Result;
73use chrono::{DateTime, Utc};
74use serde::{Deserialize, Serialize};
75use std::collections::HashMap;
76use std::path::PathBuf;
77use std::sync::Arc;
78use tokio::sync::RwLock;
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum DiskType {
83 Private,
85 Public,
87 Shared,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DiskConfig {
94 pub max_size: u64,
96 pub encrypted: bool,
98 pub fec: FecParams,
100 pub auto_sync_interval: Option<u64>,
102}
103
104impl Default for DiskConfig {
105 fn default() -> Self {
106 Self {
107 max_size: 1_073_741_824, encrypted: true,
109 fec: FecParams {
110 k: 4, m: 2, shard_size: 65536, },
114 auto_sync_interval: Some(300), }
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct DiskHandle {
122 pub entity_id: Key,
124 pub disk_type: DiskType,
126 pub config: DiskConfig,
128 pub root_manifest: Key,
130 state: Arc<RwLock<DiskState>>,
132}
133
134#[derive(Debug)]
136struct DiskState {
137 files: HashMap<PathBuf, FileEntry>,
139 used_space: u64,
141 last_sync: DateTime<Utc>,
143 dirty: bool,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct FileEntry {
150 pub path: PathBuf,
152 pub size: u64,
154 pub content_hash: Key,
156 pub metadata: FileMetadata,
158 pub created_at: DateTime<Utc>,
160 pub modified_at: DateTime<Utc>,
162 pub is_directory: bool,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct FileMetadata {
169 pub mime_type: Option<String>,
171 pub attributes: HashMap<String, String>,
173 pub permissions: u32,
175}
176
177impl Default for FileMetadata {
178 fn default() -> Self {
179 Self {
180 mime_type: None,
181 attributes: HashMap::new(),
182 permissions: 0o644,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WriteReceipt {
190 pub path: PathBuf,
192 pub content_hash: Key,
194 pub bytes_written: u64,
196 pub timestamp: DateTime<Utc>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct SyncStatus {
203 pub files_synced: usize,
205 pub bytes_synced: u64,
207 pub timestamp: DateTime<Utc>,
209 pub errors: Vec<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Asset {
216 pub path: String,
218 pub content: Vec<u8>,
220 pub mime_type: String,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PublishReceipt {
227 pub entity_id: Key,
229 pub website_root: Key,
231 pub manifest_key: Key,
233 pub timestamp: DateTime<Utc>,
235}
236
237pub async fn disk_create(
239 entity_id: Key,
240 disk_type: DiskType,
241 config: DiskConfig,
242) -> Result<DiskHandle> {
243 let mut hash_input = Vec::new();
245 hash_input.extend_from_slice(entity_id.as_bytes());
246 hash_input.push(disk_type as u8);
247 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
248
249 let state = Arc::new(RwLock::new(DiskState {
251 files: HashMap::new(),
252 used_space: 0,
253 last_sync: Utc::now(),
254 dirty: false,
255 }));
256
257 let manifest = ContainerManifestV1 {
259 v: 1,
260 object: root_manifest.clone(),
261 fec: Some(config.fec.clone()),
262 assets: Vec::new(),
263 sealed_meta: if config.encrypted {
264 Some(Key::from([0u8; 32])) } else {
266 None
267 },
268 };
269
270 #[cfg(any(test, feature = "test-utils"))]
272 {
273 use crate::mock_dht::mock_ops;
274 mock_ops::container_manifest_put(
275 &manifest,
276 &PutPolicy {
277 quorum: 3,
278 ttl: None,
279 },
281 )
282 .await?;
283 }
284
285 #[cfg(not(any(test, feature = "test-utils")))]
286 {
287 let fec_params = manifest.fec.as_ref().unwrap_or(&config.fec);
288 container_manifest_put(
289 &manifest,
290 fec_params,
291 &PutPolicy {
292 quorum: 3,
293 ttl: None,
294 },
296 )
297 .await?;
298 }
299
300 Ok(DiskHandle {
301 entity_id,
302 disk_type,
303 config,
304 root_manifest,
305 state,
306 })
307}
308
309pub async fn disk_mount(entity_id: Key, disk_type: DiskType) -> Result<DiskHandle> {
311 let mut hash_input = Vec::new();
313 hash_input.extend_from_slice(entity_id.as_bytes());
314 hash_input.push(disk_type as u8);
315 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
316
317 #[cfg(any(test, feature = "test-utils"))]
319 let _manifest =
320 crate::mock_dht::mock_ops::container_manifest_fetch(root_manifest.as_bytes()).await?;
321
322 #[cfg(not(any(test, feature = "test-utils")))]
323 let _manifest = container_manifest_fetch(root_manifest.as_bytes()).await?;
324
325 let state = Arc::new(RwLock::new(DiskState {
328 files: HashMap::new(),
329 used_space: 0,
330 last_sync: Utc::now(),
331 dirty: false,
332 }));
333
334 Ok(DiskHandle {
335 entity_id,
336 disk_type,
337 config: DiskConfig::default(),
338 root_manifest,
339 state,
340 })
341}
342
343pub async fn disk_write(
345 handle: &DiskHandle,
346 path: &str,
347 content: &[u8],
348 metadata: FileMetadata,
349) -> Result<WriteReceipt> {
350 let path_buf = PathBuf::from(path);
351
352 let mut state = handle.state.write().await;
354 if state.used_space + content.len() as u64 > handle.config.max_size {
355 anyhow::bail!("Disk space exceeded");
356 }
357
358 let content_hash = Key::from(*blake3::hash(content).as_bytes());
360
361 #[cfg(any(test, feature = "test-utils"))]
366 {
367 let stored_content = if handle.config.encrypted {
369 content.to_vec()
371 } else {
372 content.to_vec()
373 };
374 let pol = PutPolicy {
375 quorum: 3,
376 ttl: None,
377 };
379 crate::mock_dht::mock_ops::dht_put(
380 content_hash.clone(),
381 bytes::Bytes::from(stored_content),
382 &pol,
383 )
384 .await?;
385 }
386
387 #[cfg(not(any(test, feature = "test-utils")))]
388 {
389 let _pol = PutPolicy {
390 quorum: 3,
391 ttl: None,
392 };
394 }
402
403 let now = Utc::now();
405 let entry = FileEntry {
406 path: path_buf.clone(),
407 size: content.len() as u64,
408 content_hash: content_hash.clone(),
409 metadata,
410 created_at: state
411 .files
412 .get(&path_buf)
413 .map(|e| e.created_at)
414 .unwrap_or(now),
415 modified_at: now,
416 is_directory: false,
417 };
418
419 if let Some(old_entry) = state.files.insert(path_buf.clone(), entry) {
421 state.used_space -= old_entry.size;
422 }
423 state.used_space += content.len() as u64;
424 state.dirty = true;
425
426 Ok(WriteReceipt {
427 path: path_buf,
428 content_hash,
429 bytes_written: content.len() as u64,
430 timestamp: now,
431 })
432}
433
434pub async fn disk_read(handle: &DiskHandle, path: &str) -> Result<Vec<u8>> {
436 let path_buf = PathBuf::from(path);
437
438 let state = handle.state.read().await;
440 let entry = state
441 .files
442 .get(&path_buf)
443 .ok_or_else(|| anyhow::anyhow!("File not found: {}", path))?;
444
445 if entry.is_directory {
446 anyhow::bail!("Path is a directory: {}", path);
447 }
448
449 #[cfg(any(test, feature = "test-utils"))]
451 let content = crate::mock_dht::mock_ops::dht_get(entry.content_hash.clone(), 1).await?;
452
453 #[cfg(not(any(test, feature = "test-utils")))]
454 let content = bytes::Bytes::from(vec![]);
457
458 let decrypted = if handle.config.encrypted {
460 content.to_vec()
462 } else {
463 content.to_vec()
464 };
465
466 Ok(decrypted)
467}
468
469pub async fn disk_list(handle: &DiskHandle, path: &str, recursive: bool) -> Result<Vec<FileEntry>> {
471 let search_path = if path == "." {
472 PathBuf::new() } else {
474 PathBuf::from(path)
475 };
476 let state = handle.state.read().await;
477
478 let mut results = Vec::new();
479
480 for (file_path, entry) in &state.files {
481 if recursive {
483 if search_path.as_os_str().is_empty() || file_path.starts_with(&search_path) {
485 results.push(entry.clone());
486 }
487 } else {
488 if let Some(parent) = file_path.parent() {
490 if parent == search_path {
491 results.push(entry.clone());
492 }
493 } else if search_path.as_os_str().is_empty() {
494 results.push(entry.clone());
496 }
497 }
498 }
499
500 results.sort_by(|a, b| a.path.cmp(&b.path));
502
503 Ok(results)
504}
505
506pub async fn disk_delete(handle: &DiskHandle, path: &str) -> Result<()> {
508 let path_buf = PathBuf::from(path);
509
510 let mut state = handle.state.write().await;
511
512 if let Some(entry) = state.files.remove(&path_buf) {
514 state.used_space -= entry.size;
515 state.dirty = true;
516 Ok(())
517 } else {
518 Err(anyhow::anyhow!("File not found: {}", path))
519 }
520}
521
522pub async fn disk_sync(handle: &DiskHandle) -> Result<SyncStatus> {
524 let mut state = handle.state.write().await;
525
526 if !state.dirty {
527 return Ok(SyncStatus {
528 files_synced: 0,
529 bytes_synced: 0,
530 timestamp: Utc::now(),
531 errors: Vec::new(),
532 });
533 }
534
535 let file_list: Vec<FileEntry> = state.files.values().cloned().collect();
537 let tree_bytes = serde_cbor::to_vec(&file_list)?;
538 let tree_hash = Key::from(*blake3::hash(&tree_bytes).as_bytes());
539
540 #[cfg(any(test, feature = "test-utils"))]
542 {
543 let pol = PutPolicy {
544 quorum: 3,
545 ttl: None,
546 };
548 crate::mock_dht::mock_ops::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol)
549 .await?;
550
551 let manifest = ContainerManifestV1 {
553 v: 1,
554 object: handle.root_manifest.clone(),
555 fec: Some(handle.config.fec.clone()),
556 assets: vec![tree_hash],
557 sealed_meta: if handle.config.encrypted {
558 Some(Key::from([0u8; 32])) } else {
560 None
561 },
562 };
563
564 crate::mock_dht::mock_ops::container_manifest_put(&manifest, &pol).await?;
565 }
566
567 #[cfg(not(any(test, feature = "test-utils")))]
568 {
569 let pol = PutPolicy {
570 quorum: 3,
571 ttl: None,
572 };
574 let manifest = ContainerManifestV1 {
579 v: 1,
580 object: handle.root_manifest.clone(),
581 fec: Some(handle.config.fec.clone()),
582 assets: vec![tree_hash],
583 sealed_meta: if handle.config.encrypted {
584 Some(Key::from([0u8; 32])) } else {
586 None
587 },
588 };
589
590 let fec_params = manifest.fec.as_ref().unwrap_or(&handle.config.fec);
591 container_manifest_put(&manifest, fec_params, &pol).await?;
592 }
593
594 state.dirty = false;
596 state.last_sync = Utc::now();
597
598 Ok(SyncStatus {
599 files_synced: state.files.len(),
600 bytes_synced: state.used_space,
601 timestamp: state.last_sync,
602 errors: Vec::new(),
603 })
604}
605
606pub async fn website_set_home(
608 handle: &DiskHandle,
609 markdown_content: &str,
610 assets: Vec<Asset>,
611) -> Result<()> {
612 disk_write(
614 handle,
615 "home.md",
616 markdown_content.as_bytes(),
617 FileMetadata {
618 mime_type: Some("text/markdown".to_string()),
619 ..Default::default()
620 },
621 )
622 .await?;
623
624 for asset in assets {
626 disk_write(
627 handle,
628 &asset.path,
629 &asset.content,
630 FileMetadata {
631 mime_type: Some(asset.mime_type),
632 ..Default::default()
633 },
634 )
635 .await?;
636 }
637
638 disk_sync(handle).await?;
640
641 Ok(())
642}
643
644pub async fn website_publish(entity_id: Key, website_root: Key) -> Result<PublishReceipt> {
646 let handle = match disk_mount(entity_id.clone(), DiskType::Public).await {
648 Ok(h) => h,
649 Err(_) => disk_create(entity_id.clone(), DiskType::Public, DiskConfig::default()).await?,
650 };
651
652 disk_sync(&handle).await?;
654
655 Ok(PublishReceipt {
660 entity_id,
661 website_root,
662 manifest_key: handle.root_manifest,
663 timestamp: Utc::now(),
664 })
665}