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
34#[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    // Serialize the manifest
43    let manifest_bytes = bincode::serialize(manifest)?;
44
45    // Compute the key
46    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    // Store in DHT (would use actual DHT API)
52    // For now, we'll use a placeholder implementation
53    // In production, this would call the actual DHT storage
54
55    Ok(key)
56}
57
58/// Fetch a container manifest from the DHT
59#[allow(dead_code)]
60async fn container_manifest_fetch(_key: &[u8]) -> Result<ContainerManifestV1> {
61    // In production, this would fetch from actual DHT
62    // For now, return a default manifest
63    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/// Disk type for virtual disk instances
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum DiskType {
85    /// Private encrypted disk for user data
86    Private,
87    /// Public disk for website/content hosting
88    Public,
89    /// Shared disk with access control
90    Shared,
91}
92
93/// Configuration for virtual disk
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct DiskConfig {
96    /// Maximum disk size in bytes
97    pub max_size: u64,
98    /// Encryption enabled
99    pub encrypted: bool,
100    /// FEC parameters for redundancy
101    pub fec: FecParams,
102    /// Auto-sync interval in seconds
103    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, // 1GB default
110            encrypted: true,
111            fec: FecParams {
112                k: 4,              // 4 data shards
113                m: 2,              // 2 parity shards
114                shard_size: 65536, // 64KB shards
115            },
116            auto_sync_interval: Some(300), // 5 minutes
117        }
118    }
119}
120
121/// Handle to an active virtual disk
122#[derive(Debug, Clone)]
123pub struct DiskHandle {
124    /// Entity ID owning this disk
125    pub entity_id: Key,
126    /// Disk type
127    pub disk_type: DiskType,
128    /// Disk configuration
129    pub config: DiskConfig,
130    /// Root manifest key
131    pub root_manifest: Key,
132    /// Internal state
133    state: Arc<RwLock<DiskState>>,
134}
135
136/// Internal disk state
137#[derive(Debug)]
138struct DiskState {
139    /// File system tree
140    files: HashMap<PathBuf, FileEntry>,
141    /// Total used space
142    used_space: u64,
143    /// Last sync time
144    last_sync: DateTime<Utc>,
145    /// Dirty flag for pending changes
146    dirty: bool,
147}
148
149/// File entry in virtual disk
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct FileEntry {
152    /// File path
153    pub path: PathBuf,
154    /// File size in bytes
155    pub size: u64,
156    /// Content hash
157    pub content_hash: Key,
158    /// File metadata
159    pub metadata: FileMetadata,
160    /// Creation time
161    pub created_at: DateTime<Utc>,
162    /// Modification time
163    pub modified_at: DateTime<Utc>,
164    /// Is directory
165    pub is_directory: bool,
166}
167
168/// File metadata
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct FileMetadata {
171    /// MIME type
172    pub mime_type: Option<String>,
173    /// Custom attributes
174    pub attributes: HashMap<String, String>,
175    /// Permissions (Unix-style)
176    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/// Write receipt for disk operations
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct WriteReceipt {
192    /// Written file path
193    pub path: PathBuf,
194    /// Content hash
195    pub content_hash: Key,
196    /// Bytes written
197    pub bytes_written: u64,
198    /// Timestamp
199    pub timestamp: DateTime<Utc>,
200}
201
202/// Sync status for disk synchronization
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct SyncStatus {
205    /// Files synced
206    pub files_synced: usize,
207    /// Bytes synced
208    pub bytes_synced: u64,
209    /// Sync timestamp
210    pub timestamp: DateTime<Utc>,
211    /// Any errors encountered
212    pub errors: Vec<String>,
213}
214
215/// Asset for website publishing
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Asset {
218    /// Asset path (relative)
219    pub path: String,
220    /// Asset content
221    pub content: Vec<u8>,
222    /// MIME type
223    pub mime_type: String,
224}
225
226/// Publish receipt for website publishing
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PublishReceipt {
229    /// Entity ID
230    pub entity_id: Key,
231    /// Website root key
232    pub website_root: Key,
233    /// Manifest key
234    pub manifest_key: Key,
235    /// Timestamp
236    pub timestamp: DateTime<Utc>,
237}
238
239/// Create a new virtual disk
240pub async fn disk_create(
241    entity_id: Key,
242    disk_type: DiskType,
243    config: DiskConfig,
244) -> Result<DiskHandle> {
245    // Generate root manifest key
246    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    // Initialize disk state
252    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    // Create initial manifest
260    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])) // Placeholder for sealed metadata
267        } else {
268            None
269        },
270    };
271
272    // Store manifest in DHT
273    #[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                // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
282            },
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                // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
297            },
298        )
299        .await?;
300    }
301
302    Ok(DiskHandle {
303        entity_id,
304        disk_type,
305        config,
306        root_manifest,
307        state,
308    })
309}
310
311/// Mount an existing virtual disk
312pub async fn disk_mount(entity_id: Key, disk_type: DiskType) -> Result<DiskHandle> {
313    // Derive root manifest key
314    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    // Fetch manifest from DHT
320    #[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    // Load disk state from manifest
328    // TODO: Deserialize file tree from manifest assets
329    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
345/// Write a file to the virtual disk
346pub 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    // Check disk space
355    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    // Hash content
361    let content_hash = Key::from(*blake3::hash(content).as_bytes());
362
363    // Apply FEC encoding / encryption (handled in build-specific branches below)
364    // TODO: Use saorsa-fec for forward error correction
365
366    // Store in DHT
367    #[cfg(any(test, feature = "test-utils"))]
368    {
369        // Encrypt if needed
370        let stored_content = if handle.config.encrypted {
371            // TODO: Use saorsa-seal for encryption
372            content.to_vec()
373        } else {
374            content.to_vec()
375        };
376        let pol = PutPolicy {
377            quorum: 3,
378            ttl: None,
379            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
380        };
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            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
395        };
396        // TODO: Update to use new clean API
397        // crate::api::dht_put(
398        //     content_hash.clone(),
399        //     bytes::Bytes::from(stored_content),
400        //     &pol,
401        // )
402        // .await?;
403    }
404
405    // Update file tree
406    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    // Update state
422    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
436/// Read a file from the virtual disk
437pub async fn disk_read(handle: &DiskHandle, path: &str) -> Result<Vec<u8>> {
438    let path_buf = PathBuf::from(path);
439
440    // Find file in tree
441    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    // Fetch from DHT
452    #[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    // TODO: Update to use new clean API
457    // let content = crate::api::dht_get(entry.content_hash.clone(), 1).await?;
458    let content = bytes::Bytes::from(vec![]);
459
460    // Decrypt if needed
461    let decrypted = if handle.config.encrypted {
462        // TODO: Use saorsa-seal for decryption
463        content.to_vec()
464    } else {
465        content.to_vec()
466    };
467
468    Ok(decrypted)
469}
470
471/// List files in a directory
472pub async fn disk_list(handle: &DiskHandle, path: &str, recursive: bool) -> Result<Vec<FileEntry>> {
473    let search_path = if path == "." {
474        PathBuf::new() // Use empty path for current directory
475    } 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        // Check if file is in the requested directory
484        if recursive {
485            // Include if path is ancestor or if we're listing from root
486            if search_path.as_os_str().is_empty() || file_path.starts_with(&search_path) {
487                results.push(entry.clone());
488            }
489        } else {
490            // Include only direct children
491            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                // File has no parent (is in root), and we're searching root
497                results.push(entry.clone());
498            }
499        }
500    }
501
502    // Sort by path
503    results.sort_by(|a, b| a.path.cmp(&b.path));
504
505    Ok(results)
506}
507
508/// Delete a file from the virtual disk
509pub 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    // Remove file from tree
515    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
524/// Synchronize disk state to DHT
525pub 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    // Serialize file tree
538    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    // Store tree in DHT
543    #[cfg(any(test, feature = "test-utils"))]
544    {
545        let pol = PutPolicy {
546            quorum: 3,
547            ttl: None,
548            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
549        };
550        crate::mock_dht::mock_ops::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol)
551            .await?;
552
553        // Update manifest
554        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])) // Placeholder
561            } 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            // auth: Box::new(crate::auth::DelegatedWriteAuth::new(vec![])),
575        };
576        // TODO: Update to use new clean API
577        // crate::api::dht_put(tree_hash.clone(), bytes::Bytes::from(tree_bytes), &pol).await?;
578
579        // Update manifest
580        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])) // Placeholder
587            } 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    // Update state
597    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
608/// Set the home page for a website
609pub async fn website_set_home(
610    handle: &DiskHandle,
611    markdown_content: &str,
612    assets: Vec<Asset>,
613) -> Result<()> {
614    // Write home.md
615    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    // Write assets
627    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    // Sync to ensure persistence
641    disk_sync(handle).await?;
642
643    Ok(())
644}
645
646/// Publish a website from a disk
647pub async fn website_publish(entity_id: Key, website_root: Key) -> Result<PublishReceipt> {
648    // Create public disk if not exists
649    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    // Sync disk to ensure latest state
655    disk_sync(&handle).await?;
656
657    // Optionally update identity with website root
658    // Note: This requires the caller to have signing material
659    // identity_set_website_root(entity_id.clone(), website_root.clone(), sig).await?;
660
661    Ok(PublishReceipt {
662        entity_id,
663        website_root,
664        manifest_key: handle.root_manifest,
665        timestamp: Utc::now(),
666    })
667}