1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct ContainerManifestV1 {
22 pub v: u8,
23 pub object: Key,
24 pub fec: Option<FecParams>,
25 pub assets: Vec<Key>,
26 pub sealed_meta: Option<Key>,
27}
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct FecParams {
30 pub k: usize,
31 pub m: usize,
32 pub shard_size: usize,
33}
34#[allow(dead_code)]
35async fn container_manifest_put(_: &ContainerManifestV1, _: &FecParams, _: &PutPolicy) -> Result<Key> {
36 unimplemented!("Virtual disk not yet migrated")
37}
38#[allow(dead_code)]
39async fn container_manifest_fetch(_: &[u8]) -> Result<ContainerManifestV1> {
40 unimplemented!("Virtual disk not yet migrated")
41}
42
43use crate::dht::PutPolicy;
44use crate::fwid::Key;
45use anyhow::Result;
46use chrono::{DateTime, Utc};
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use std::path::PathBuf;
50use std::sync::Arc;
51use tokio::sync::RwLock;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum DiskType {
56 Private,
58 Public,
60 Shared,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DiskConfig {
67 pub max_size: u64,
69 pub encrypted: bool,
71 pub fec: FecParams,
73 pub auto_sync_interval: Option<u64>,
75}
76
77impl Default for DiskConfig {
78 fn default() -> Self {
79 Self {
80 max_size: 1_073_741_824, encrypted: true,
82 fec: FecParams {
83 k: 4, m: 2, shard_size: 65536, },
87 auto_sync_interval: Some(300), }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct DiskHandle {
95 pub entity_id: Key,
97 pub disk_type: DiskType,
99 pub config: DiskConfig,
101 pub root_manifest: Key,
103 state: Arc<RwLock<DiskState>>,
105}
106
107#[derive(Debug)]
109struct DiskState {
110 files: HashMap<PathBuf, FileEntry>,
112 used_space: u64,
114 last_sync: DateTime<Utc>,
116 dirty: bool,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct FileEntry {
123 pub path: PathBuf,
125 pub size: u64,
127 pub content_hash: Key,
129 pub metadata: FileMetadata,
131 pub created_at: DateTime<Utc>,
133 pub modified_at: DateTime<Utc>,
135 pub is_directory: bool,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct FileMetadata {
142 pub mime_type: Option<String>,
144 pub attributes: HashMap<String, String>,
146 pub permissions: u32,
148}
149
150impl Default for FileMetadata {
151 fn default() -> Self {
152 Self {
153 mime_type: None,
154 attributes: HashMap::new(),
155 permissions: 0o644,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct WriteReceipt {
163 pub path: PathBuf,
165 pub content_hash: Key,
167 pub bytes_written: u64,
169 pub timestamp: DateTime<Utc>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SyncStatus {
176 pub files_synced: usize,
178 pub bytes_synced: u64,
180 pub timestamp: DateTime<Utc>,
182 pub errors: Vec<String>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Asset {
189 pub path: String,
191 pub content: Vec<u8>,
193 pub mime_type: String,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct PublishReceipt {
200 pub entity_id: Key,
202 pub website_root: Key,
204 pub manifest_key: Key,
206 pub timestamp: DateTime<Utc>,
208}
209
210pub async fn disk_create(
212 entity_id: Key,
213 disk_type: DiskType,
214 config: DiskConfig,
215) -> Result<DiskHandle> {
216 let mut hash_input = Vec::new();
218 hash_input.extend_from_slice(entity_id.as_bytes());
219 hash_input.push(disk_type as u8);
220 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
221
222 let state = Arc::new(RwLock::new(DiskState {
224 files: HashMap::new(),
225 used_space: 0,
226 last_sync: Utc::now(),
227 dirty: false,
228 }));
229
230 let manifest = ContainerManifestV1 {
232 v: 1,
233 object: root_manifest.clone(),
234 fec: Some(config.fec.clone()),
235 assets: Vec::new(),
236 sealed_meta: if config.encrypted {
237 Some(Key::from([0u8; 32])) } else {
239 None
240 },
241 };
242
243 #[cfg(any(test, feature = "test-utils"))]
245 {
246 use crate::mock_dht::mock_ops;
247 mock_ops::container_manifest_put(
248 &manifest,
249 &PutPolicy {
250 quorum: 3,
251 ttl: None,
252 },
254 )
255 .await?;
256 }
257
258 #[cfg(not(any(test, feature = "test-utils")))]
259 {
260 let fec_params = manifest.fec.as_ref().unwrap_or(&config.fec);
261 container_manifest_put(
262 &manifest,
263 fec_params,
264 &PutPolicy {
265 quorum: 3,
266 ttl: None,
267 },
269 )
270 .await?;
271 }
272
273 Ok(DiskHandle {
274 entity_id,
275 disk_type,
276 config,
277 root_manifest,
278 state,
279 })
280}
281
282pub async fn disk_mount(entity_id: Key, disk_type: DiskType) -> Result<DiskHandle> {
284 let mut hash_input = Vec::new();
286 hash_input.extend_from_slice(entity_id.as_bytes());
287 hash_input.push(disk_type as u8);
288 let root_manifest = Key::from(*blake3::hash(&hash_input).as_bytes());
289
290 #[cfg(any(test, feature = "test-utils"))]
292 let _manifest =
293 crate::mock_dht::mock_ops::container_manifest_fetch(root_manifest.as_bytes()).await?;
294
295 #[cfg(not(any(test, feature = "test-utils")))]
296 let _manifest = container_manifest_fetch(root_manifest.as_bytes()).await?;
297
298 let state = Arc::new(RwLock::new(DiskState {
301 files: HashMap::new(),
302 used_space: 0,
303 last_sync: Utc::now(),
304 dirty: false,
305 }));
306
307 Ok(DiskHandle {
308 entity_id,
309 disk_type,
310 config: DiskConfig::default(),
311 root_manifest,
312 state,
313 })
314}
315
316pub async fn disk_write(
318 handle: &DiskHandle,
319 path: &str,
320 content: &[u8],
321 metadata: FileMetadata,
322) -> Result<WriteReceipt> {
323 let path_buf = PathBuf::from(path);
324
325 let mut state = handle.state.write().await;
327 if state.used_space + content.len() as u64 > handle.config.max_size {
328 anyhow::bail!("Disk space exceeded");
329 }
330
331 let content_hash = Key::from(*blake3::hash(content).as_bytes());
333
334 #[cfg(any(test, feature = "test-utils"))]
339 {
340 let stored_content = if handle.config.encrypted {
342 content.to_vec()
344 } else {
345 content.to_vec()
346 };
347 let pol = PutPolicy {
348 quorum: 3,
349 ttl: None,
350 };
352 crate::mock_dht::mock_ops::dht_put(
353 content_hash.clone(),
354 bytes::Bytes::from(stored_content),
355 &pol,
356 )
357 .await?;
358 }
359
360 #[cfg(not(any(test, feature = "test-utils")))]
361 {
362 let _pol = PutPolicy {
363 quorum: 3,
364 ttl: None,
365 };
367 }
375
376 let now = Utc::now();
378 let entry = FileEntry {
379 path: path_buf.clone(),
380 size: content.len() as u64,
381 content_hash: content_hash.clone(),
382 metadata,
383 created_at: state
384 .files
385 .get(&path_buf)
386 .map(|e| e.created_at)
387 .unwrap_or(now),
388 modified_at: now,
389 is_directory: false,
390 };
391
392 if let Some(old_entry) = state.files.insert(path_buf.clone(), entry) {
394 state.used_space -= old_entry.size;
395 }
396 state.used_space += content.len() as u64;
397 state.dirty = true;
398
399 Ok(WriteReceipt {
400 path: path_buf,
401 content_hash,
402 bytes_written: content.len() as u64,
403 timestamp: now,
404 })
405}
406
407pub async fn disk_read(handle: &DiskHandle, path: &str) -> Result<Vec<u8>> {
409 let path_buf = PathBuf::from(path);
410
411 let state = handle.state.read().await;
413 let entry = state
414 .files
415 .get(&path_buf)
416 .ok_or_else(|| anyhow::anyhow!("File not found: {}", path))?;
417
418 if entry.is_directory {
419 anyhow::bail!("Path is a directory: {}", path);
420 }
421
422 #[cfg(any(test, feature = "test-utils"))]
424 let content = crate::mock_dht::mock_ops::dht_get(entry.content_hash.clone(), 1).await?;
425
426 #[cfg(not(any(test, feature = "test-utils")))]
427 let content = bytes::Bytes::from(vec![]);
430
431 let decrypted = if handle.config.encrypted {
433 content.to_vec()
435 } else {
436 content.to_vec()
437 };
438
439 Ok(decrypted)
440}
441
442pub async fn disk_list(handle: &DiskHandle, path: &str, recursive: bool) -> Result<Vec<FileEntry>> {
444 let search_path = if path == "." {
445 PathBuf::new() } else {
447 PathBuf::from(path)
448 };
449 let state = handle.state.read().await;
450
451 let mut results = Vec::new();
452
453 for (file_path, entry) in &state.files {
454 if recursive {
456 if search_path.as_os_str().is_empty() || file_path.starts_with(&search_path) {
458 results.push(entry.clone());
459 }
460 } else {
461 if let Some(parent) = file_path.parent() {
463 if parent == search_path {
464 results.push(entry.clone());
465 }
466 } else if search_path.as_os_str().is_empty() {
467 results.push(entry.clone());
469 }
470 }
471 }
472
473 results.sort_by(|a, b| a.path.cmp(&b.path));
475
476 Ok(results)
477}
478
479pub async fn disk_delete(handle: &DiskHandle, path: &str) -> Result<()> {
481 let path_buf = PathBuf::from(path);
482
483 let mut state = handle.state.write().await;
484
485 if let Some(entry) = state.files.remove(&path_buf) {
487 state.used_space -= entry.size;
488 state.dirty = true;
489 Ok(())
490 } else {
491 Err(anyhow::anyhow!("File not found: {}", path))
492 }
493}
494
495pub async fn disk_sync(handle: &DiskHandle) -> Result<SyncStatus> {
497 let mut state = handle.state.write().await;
498
499 if !state.dirty {
500 return Ok(SyncStatus {
501 files_synced: 0,
502 bytes_synced: 0,
503 timestamp: Utc::now(),
504 errors: Vec::new(),
505 });
506 }
507
508 let file_list: Vec<FileEntry> = state.files.values().cloned().collect();
510 let tree_bytes = serde_cbor::to_vec(&file_list)?;
511 let tree_hash = Key::from(*blake3::hash(&tree_bytes).as_bytes());
512
513 #[cfg(any(test, feature = "test-utils"))]
515 {
516 let pol = PutPolicy {
517 quorum: 3,
518 ttl: None,
519 };
521 crate::mock_dht::mock_ops::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol)
522 .await?;
523
524 let manifest = ContainerManifestV1 {
526 v: 1,
527 object: handle.root_manifest.clone(),
528 fec: Some(handle.config.fec.clone()),
529 assets: vec![tree_hash],
530 sealed_meta: if handle.config.encrypted {
531 Some(Key::from([0u8; 32])) } else {
533 None
534 },
535 };
536
537 crate::mock_dht::mock_ops::container_manifest_put(&manifest, &pol).await?;
538 }
539
540 #[cfg(not(any(test, feature = "test-utils")))]
541 {
542 let pol = PutPolicy {
543 quorum: 3,
544 ttl: None,
545 };
547 let manifest = ContainerManifestV1 {
552 v: 1,
553 object: handle.root_manifest.clone(),
554 fec: Some(handle.config.fec.clone()),
555 assets: vec![tree_hash],
556 sealed_meta: if handle.config.encrypted {
557 Some(Key::from([0u8; 32])) } else {
559 None
560 },
561 };
562
563 let fec_params = manifest.fec.as_ref().unwrap_or(&handle.config.fec);
564 container_manifest_put(&manifest, fec_params, &pol).await?;
565 }
566
567 state.dirty = false;
569 state.last_sync = Utc::now();
570
571 Ok(SyncStatus {
572 files_synced: state.files.len(),
573 bytes_synced: state.used_space,
574 timestamp: state.last_sync,
575 errors: Vec::new(),
576 })
577}
578
579pub async fn website_set_home(
581 handle: &DiskHandle,
582 markdown_content: &str,
583 assets: Vec<Asset>,
584) -> Result<()> {
585 disk_write(
587 handle,
588 "home.md",
589 markdown_content.as_bytes(),
590 FileMetadata {
591 mime_type: Some("text/markdown".to_string()),
592 ..Default::default()
593 },
594 )
595 .await?;
596
597 for asset in assets {
599 disk_write(
600 handle,
601 &asset.path,
602 &asset.content,
603 FileMetadata {
604 mime_type: Some(asset.mime_type),
605 ..Default::default()
606 },
607 )
608 .await?;
609 }
610
611 disk_sync(handle).await?;
613
614 Ok(())
615}
616
617pub async fn website_publish(entity_id: Key, website_root: Key) -> Result<PublishReceipt> {
619 let handle = match disk_mount(entity_id.clone(), DiskType::Public).await {
621 Ok(h) => h,
622 Err(_) => disk_create(entity_id.clone(), DiskType::Public, DiskConfig::default()).await?,
623 };
624
625 disk_sync(&handle).await?;
627
628 Ok(PublishReceipt {
633 entity_id,
634 website_root,
635 manifest_key: handle.root_manifest,
636 timestamp: Utc::now(),
637 })
638}