diff --git a/src/carrier.rs b/src/carrier.rs index ca91abe..0fd3afd 100644 --- a/src/carrier.rs +++ b/src/carrier.rs @@ -885,6 +885,22 @@ impl DavFileSystem for CarrierFs { Ok(()) }) } + + fn get_quota(&self) -> FsFuture<'_, (u64, Option)> { + Box::pin(async move { + self.touch(); + let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + let total_capacity = inner.manifest.total_blocks as u64 * crate::crypto::CHUNK_SIZE as u64; + let used_bytes = inner + .manifest + .inodes + .values() + .filter(|i| !i.is_dir) + .map(|i| i.size) + .sum::(); + Ok((used_bytes, Some(total_capacity))) + }) + } } /// Datei-Handle für Dateien innerhalb des Carrier-Dateisystems mit Streaming und Chunk-Pufferung. diff --git a/src/storage.rs b/src/storage.rs index a9867e3..ca54f28 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -349,6 +349,38 @@ impl Database { self.conn() } + /// Ermittelt den physischen Dateipfad des geöffneten Containers aus SQLite (sofern nicht in-memory). + pub fn container_path(&self) -> Option { + let conn = self.conn(); + let path_str: String = conn + .query_row("PRAGMA database_list;", [], |r| r.get(2)) + .ok()?; + if path_str.is_empty() { + None + } else { + Some(std::path::PathBuf::from(path_str)) + } + } + + /// Ermittelt die physische Dateigröße des Containers über die SQLite-Page-Statistik (Z-04). + pub fn get_container_file_size(&self) -> Result { + let conn = self.conn(); + let page_count: i64 = conn.query_row("PRAGMA page_count;", [], |r| r.get(0))?; + let page_size: i64 = conn.query_row("PRAGMA page_size;", [], |r| r.get(0))?; + Ok((page_count.saturating_mul(page_size)).max(0) as u64) + } + + /// Ermittelt die kumulierte Dateigröße aller im Container gespeicherten Nutzdateien (Z-04). + pub fn get_total_used_size(&self) -> Result { + let conn = self.conn(); + let total: i64 = conn.query_row( + "SELECT COALESCE(SUM(size), 0) FROM nodes WHERE is_dir = 0", + [], + |r| r.get(0), + )?; + Ok(total.max(0) as u64) + } + /// Erzeugt eine geklonte Instanz mit einer isolierten aktiven Session (Slot & DEK). pub fn with_session(&self, slot_id: u32, dek: Zeroizing<[u8; 32]>) -> Self { Self { diff --git a/src/vfs.rs b/src/vfs.rs index 0bee7b8..b91ba7e 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -1127,10 +1127,28 @@ impl DavFileSystem for SanctumFs { } fn get_quota(&self) -> FsFuture<'_, (u64, Option)> { + if let Some(ref cfs) = self.carrier_fs { + return cfs.get_quota(); + } + Box::pin(async move { - // Virtueller Speicherplatz für Explorer: 1 TB - let total_capacity: u64 = 1024 * 1024 * 1024 * 1024; - Ok((0, Some(total_capacity))) + self.touch(); + // Z-04: Echte Containergröße (physisch auf Disk) und freier Host-Speicher + let container_size = self.db.get_container_file_size().unwrap_or(0); + let used_bytes = if container_size > 0 { + container_size + } else { + self.db.get_total_used_size().unwrap_or(0) + }; + + let free_host_space = self + .db + .container_path() + .and_then(|p| crate::windows::get_available_disk_space(&p)) + .unwrap_or(1024 * 1024 * 1024 * 1024); // Fallback: 1 TB + + let total_capacity = used_bytes.saturating_add(free_host_space); + Ok((used_bytes, Some(total_capacity))) }) } } diff --git a/src/windows.rs b/src/windows.rs index 403df65..52cadf3 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -846,6 +846,55 @@ pub fn is_process_alive(pid: u32) -> bool { } } +/// Ermittelt den verfügbaren freien Speicherplatz auf dem Dateisystem des angegebenen Pfads (Z-04). +pub fn get_available_disk_space>(path: P) -> Option { + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + let p = path.as_ref(); + let dir = if p.is_dir() { + p.to_path_buf() + } else { + p.parent().map(|parent| parent.to_path_buf()).unwrap_or_else(|| p.to_path_buf()) + }; + let mut wide: Vec = dir.as_os_str().encode_wide().collect(); + wide.push(0); + + let mut free_bytes_available = 0u64; + let mut total_number_of_bytes = 0u64; + let mut total_number_of_free_bytes = 0u64; + + extern "system" { + fn GetDiskFreeSpaceExW( + lpDirectoryName: *const u16, + lpFreeBytesAvailableToCaller: *mut u64, + lpTotalNumberOfBytes: *mut u64, + lpTotalNumberOfFreeBytes: *mut u64, + ) -> i32; + } + + let success = unsafe { + GetDiskFreeSpaceExW( + wide.as_ptr(), + &mut free_bytes_available, + &mut total_number_of_bytes, + &mut total_number_of_free_bytes, + ) + }; + + if success != 0 { + Some(free_bytes_available) + } else { + None + } + } + #[cfg(not(windows))] + { + let _ = path; + Some(1024 * 1024 * 1024 * 1024) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/mount_security_test.rs b/tests/mount_security_test.rs index 49c0f1c..496b845 100644 --- a/tests/mount_security_test.rs +++ b/tests/mount_security_test.rs @@ -242,4 +242,73 @@ fn test_z01_decoy_password_zeroize_memory() { drop(ephemeral); } +#[tokio::test] +async fn test_z04_webdav_quota_report() { + use dav_server::fs::DavFileSystem; + use sanctum::vfs::SanctumFs; + + let temp_dir = std::env::temp_dir(); + let container_path: PathBuf = + temp_dir.join(format!("test_z04_quota_{}.sanctum", std::process::id())); + if container_path.exists() { + let _ = std::fs::remove_file(&container_path); + } + + let salt = generate_salt(); + let kdf_params = KdfParams { + memory_cost: MIN_MEMORY_COST_KIB, + time_cost: MIN_TIME_COST, + parallelism: 1, + }; + let kek = derive_kek("TestPassZ04!", &salt, &kdf_params).unwrap(); + let dek = generate_dek(); + let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap(); + + let db = Database::open(&container_path).unwrap(); + db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag) + .unwrap(); + + // Erstelle einen Testknoten mit Chunks + let node = db.create_node(1, "payload.bin", false).unwrap(); + let dummy_chunk = vec![0xAAu8; 1024 * 1024]; // 1 MiB + db.write_chunk_and_update_size( + node.id, + 0, + 0, + &[0u8; 12], + &[0u8; 16], + &dummy_chunk, + 1024 * 1024, + 123456, + ) + .unwrap(); + + let fs = SanctumFs::new(db.clone(), dek, sanctum::crypto::FORMAT_VERSION); + + // Rufe WebDAV get_quota ab + let (used, total_opt) = fs.get_quota().await.unwrap(); + + assert!( + used > 0, + "Z-04: Verwendeter Speicherplatz muss > 0 sein (gemeldet: {} Bytes)", + used + ); + assert!( + total_opt.is_some(), + "Z-04: Gesamtkapazität muss gemeldet werden" + ); + let total = total_opt.unwrap(); + assert!( + total >= used, + "Z-04: Gesamtkapazität ({}) muss mindestens dem belegten Speicher ({}) entsprechen", + total, + used + ); + + drop(fs); + drop(db); + let _ = std::fs::remove_file(&container_path); +} + +