saorsa_core/
virtual_disk.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: saorsalabs@gmail.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Virtual Disk API implementation for encrypted file storage
15
16// Container manifest for virtual disk storage
17#[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/// Store a container manifest in the DHT
34async fn container_manifest_put(
35    manifest: &ContainerManifestV1,
36    _fec: &FecParams,
37    _policy: &PutPolicy,
38) -> Result<Key> {
39    use blake3::Hasher;
40
41    // Serialize the manifest
42    let manifest_bytes = bincode::serialize(manifest)?;
43
44    // Compute the key
45    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    // Store in DHT (would use actual DHT API)
51    // For now, we'll use a placeholder implementation
52    // In production, this would call the actual DHT storage
53
54    Ok(key)
55}
56
57/// Fetch a container manifest from the DHT
58async fn container_manifest_fetch(_key: &[u8]) -> Result<ContainerManifestV1> {
59    // In production, this would fetch from actual DHT
60    // For now, return a default manifest
61    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/// Disk type for virtual disk instances
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum DiskType {
83    /// Private encrypted disk for user data
84    Private,
85    /// Public disk for website/content hosting
86    Public,
87    /// Shared disk with access control
88    Shared,
89}
90
91/// Configuration for virtual disk
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DiskConfig {
94    /// Maximum disk size in bytes
95    pub max_size: u64,
96    /// Encryption enabled
97    pub encrypted: bool,
98    /// FEC parameters for redundancy
99    pub fec: FecParams,
100    /// Auto-sync interval in seconds
101    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, // 1GB default
108            encrypted: true,
109            fec: FecParams {
110                k: 4,              // 4 data shards
111                m: 2,              // 2 parity shards
112                shard_size: 65536, // 64KB shards
113            },
114            auto_sync_interval: Some(300), // 5 minutes
115        }
116    }
117}
118
119/// Handle to an active virtual disk
120#[derive(Debug, Clone)]
121pub struct DiskHandle {
122    /// Entity ID owning this disk
123    pub entity_id: Key,
124    /// Disk type
125    pub disk_type: DiskType,
126    /// Disk configuration
127    pub config: DiskConfig,
128    /// Root manifest key
129    pub root_manifest: Key,
130    /// Internal state
131    state: Arc<RwLock<DiskState>>,
132}
133
134/// Internal disk state
135#[derive(Debug)]
136struct DiskState {
137    /// File system tree
138    files: HashMap<PathBuf, FileEntry>,
139    /// Total used space
140    used_space: u64,
141    /// Last sync time
142    last_sync: DateTime<Utc>,
143    /// Dirty flag for pending changes
144    dirty: bool,
145}
146
147/// File entry in virtual disk
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct FileEntry {
150    /// File path
151    pub path: PathBuf,
152    /// File size in bytes
153    pub size: u64,
154    /// Content hash
155    pub content_hash: Key,
156    /// File metadata
157    pub metadata: FileMetadata,
158    /// Creation time
159    pub created_at: DateTime<Utc>,
160    /// Modification time
161    pub modified_at: DateTime<Utc>,
162    /// Is directory
163    pub is_directory: bool,
164}
165
166/// File metadata
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct FileMetadata {
169    /// MIME type
170    pub mime_type: Option<String>,
171    /// Custom attributes
172    pub attributes: HashMap<String, String>,
173    /// Permissions (Unix-style)
174    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/// Write receipt for disk operations
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WriteReceipt {
190    /// Written file path
191    pub path: PathBuf,
192    /// Content hash
193    pub content_hash: Key,
194    /// Bytes written
195    pub bytes_written: u64,
196    /// Timestamp
197    pub timestamp: DateTime<Utc>,
198}
199
200/// Sync status for disk synchronization
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct SyncStatus {
203    /// Files synced
204    pub files_synced: usize,
205    /// Bytes synced
206    pub bytes_synced: u64,
207    /// Sync timestamp
208    pub timestamp: DateTime<Utc>,
209    /// Any errors encountered
210    pub errors: Vec<String>,
211}
212
213/// Asset for website publishing
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Asset {
216    /// Asset path (relative)
217    pub path: String,
218    /// Asset content
219    pub content: Vec<u8>,
220    /// MIME type
221    pub mime_type: String,
222}
223
224/// Publish receipt for website publishing
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PublishReceipt {
227    /// Entity ID
228    pub entity_id: Key,
229    /// Website root key
230    pub website_root: Key,
231    /// Manifest key
232    pub manifest_key: Key,
233    /// Timestamp
234    pub timestamp: DateTime<Utc>,
235}
236
237/// Create a new virtual disk
238pub async fn disk_create(
239    entity_id: Key,
240    disk_type: DiskType,
241    config: DiskConfig,
242) -> Result<DiskHandle> {
243    // Generate root manifest key
244    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    // Initialize disk state
250    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    // Create initial manifest
258    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])) // Placeholder for sealed metadata
265        } else {
266            None
267        },
268    };
269
270    // Store manifest in DHT
271    #[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                // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
280            },
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                // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
295            },
296        )
297        .await?;
298    }
299
300    Ok(DiskHandle {
301        entity_id,
302        disk_type,
303        config,
304        root_manifest,
305        state,
306    })
307}
308
309/// Mount an existing virtual disk
310pub async fn disk_mount(entity_id: Key, disk_type: DiskType) -> Result<DiskHandle> {
311    // Derive root manifest key
312    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    // Fetch manifest from DHT
318    #[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    // Load disk state from manifest
326    // TODO: Deserialize file tree from manifest assets
327    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
343/// Write a file to the virtual disk
344pub 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    // Check disk space
353    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    // Hash content
359    let content_hash = Key::from(*blake3::hash(content).as_bytes());
360
361    // Apply FEC encoding / encryption (handled in build-specific branches below)
362    // TODO: Use saorsa-fec for forward error correction
363
364    // Store in DHT
365    #[cfg(any(test, feature = "test-utils"))]
366    {
367        // Encrypt if needed
368        let stored_content = if handle.config.encrypted {
369            // TODO: Use saorsa-seal for encryption
370            content.to_vec()
371        } else {
372            content.to_vec()
373        };
374        let pol = PutPolicy {
375            quorum: 3,
376            ttl: None,
377            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
378        };
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            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
393        };
394        // TODO: Update to use new clean API
395        // crate::api::dht_put(
396        //     content_hash.clone(),
397        //     bytes::Bytes::from(stored_content),
398        //     &pol,
399        // )
400        // .await?;
401    }
402
403    // Update file tree
404    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    // Update state
420    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
434/// Read a file from the virtual disk
435pub async fn disk_read(handle: &DiskHandle, path: &str) -> Result<Vec<u8>> {
436    let path_buf = PathBuf::from(path);
437
438    // Find file in tree
439    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    // Fetch from DHT
450    #[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    // TODO: Update to use new clean API
455    // let content = crate::api::dht_get(entry.content_hash.clone(), 1).await?;
456    let content = bytes::Bytes::from(vec![]);
457
458    // Decrypt if needed
459    let decrypted = if handle.config.encrypted {
460        // TODO: Use saorsa-seal for decryption
461        content.to_vec()
462    } else {
463        content.to_vec()
464    };
465
466    Ok(decrypted)
467}
468
469/// List files in a directory
470pub async fn disk_list(handle: &DiskHandle, path: &str, recursive: bool) -> Result<Vec<FileEntry>> {
471    let search_path = if path == "." {
472        PathBuf::new() // Use empty path for current directory
473    } 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        // Check if file is in the requested directory
482        if recursive {
483            // Include if path is ancestor or if we're listing from root
484            if search_path.as_os_str().is_empty() || file_path.starts_with(&search_path) {
485                results.push(entry.clone());
486            }
487        } else {
488            // Include only direct children
489            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                // File has no parent (is in root), and we're searching root
495                results.push(entry.clone());
496            }
497        }
498    }
499
500    // Sort by path
501    results.sort_by(|a, b| a.path.cmp(&b.path));
502
503    Ok(results)
504}
505
506/// Delete a file from the virtual disk
507pub 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    // Remove file from tree
513    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
522/// Synchronize disk state to DHT
523pub 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    // Serialize file tree
536    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    // Store tree in DHT
541    #[cfg(any(test, feature = "test-utils"))]
542    {
543        let pol = PutPolicy {
544            quorum: 3,
545            ttl: None,
546            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
547        };
548        crate::mock_dht::mock_ops::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol)
549            .await?;
550
551        // Update manifest
552        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])) // Placeholder
559            } 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            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
573        };
574        // TODO: Update to use new clean API
575        // crate::api::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol).await?;
576
577        // Update manifest
578        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])) // Placeholder
585            } 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    // Update state
595    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
606/// Set the home page for a website
607pub async fn website_set_home(
608    handle: &DiskHandle,
609    markdown_content: &str,
610    assets: Vec<Asset>,
611) -> Result<()> {
612    // Write home.md
613    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    // Write assets
625    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    // Sync to ensure persistence
639    disk_sync(handle).await?;
640
641    Ok(())
642}
643
644/// Publish a website from a disk
645pub async fn website_publish(entity_id: Key, website_root: Key) -> Result<PublishReceipt> {
646    // Create public disk if not exists
647    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    // Sync disk to ensure latest state
653    disk_sync(&handle).await?;
654
655    // Optionally update identity with website root
656    // Note: This requires the caller to have signing material
657    // identity_set_website_root(entity_id.clone(), website_root.clone(), sig).await?;
658
659    Ok(PublishReceipt {
660        entity_id,
661        website_root,
662        manifest_key: handle.root_manifest,
663        timestamp: Utc::now(),
664    })
665}