//! Backup Manifest - Snapshot metadata serialization //! //! Compatible with ZFS send/receive and Proxmox Backup Server format use std::path::PathBuf; use serde::{Serialize, Deserialize}; use sha2::Digest; use super::{VfsCompression}; use super::checksum::VfsChecksumFile; use super::dedup::DedupManifest; pub const MANIFEST_VERSION: u32 = 1; pub const MANIFEST_FILE: &str = ".manifest.json"; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SendFormat { #[serde(rename = "zfs_compatible")] ZfsCompatible, #[serde(rename = "custom_json")] CustomJson, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupFileEntry { pub path: String, pub size: u64, pub checksums: Option, pub dedup_hash: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EncryptionInfo { pub algorithm: String, pub enabled: bool, pub key_hash: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompressionInfo { pub algorithm: String, pub level: u32, pub original_size: u64, pub compressed_size: u64, pub ratio: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupManifest { pub version: u32, pub format: SendFormat, pub snapshot_name: String, pub created_at: u64, pub root_path: String, pub files: Vec, pub dedup_manifest: Option, pub encryption: Option, pub compression: Option, pub total_size: u64, pub stored_size: u64, pub overall_ratio: f64, } impl BackupManifest { pub fn new(snapshot_name: String, root_path: PathBuf) -> Self { Self { version: MANIFEST_VERSION, format: SendFormat::CustomJson, snapshot_name, created_at: current_time_secs(), root_path: root_path.to_string_lossy().to_string(), files: Vec::new(), dedup_manifest: None, encryption: None, compression: None, total_size: 0, stored_size: 0, overall_ratio: 1.0, } } pub fn add_file(&mut self, path: String, size: u64, checksums: Option) { self.files.push(BackupFileEntry { path, size, checksums, dedup_hash: None, }); self.total_size += size; } pub fn set_dedup(&mut self, manifest: DedupManifest) { self.dedup_manifest = Some(manifest.clone()); if manifest.original_size > 0 { let stored = (manifest.block_hashes.len() as u64) * 4096; // Approximate self.stored_size = stored; } } pub fn set_compression(&mut self, algorithm: VfsCompression, original: u64, compressed: u64) { let ratio = if original > 0 { compressed as f64 / original as f64 } else { 1.0 }; self.compression = Some(CompressionInfo { algorithm: algorithm_name(&algorithm), level: 3, original_size: original, compressed_size: compressed, ratio, }); } pub fn set_encryption(&mut self, enabled: bool, key_hash: Option) { self.encryption = Some(EncryptionInfo { algorithm: "AES-256-GCM".to_string(), enabled, key_hash, }); } pub fn calculate_ratio(&mut self) { if self.total_size > 0 && self.stored_size > 0 { self.overall_ratio = self.stored_size as f64 / self.total_size as f64; } } pub fn to_bytes(&self) -> Result, String> { serde_json::to_vec(self).map_err(|e| e.to_string()) } pub fn from_bytes(data: &[u8]) -> Result { serde_json::from_slice(data).map_err(|e| e.to_string()) } pub fn save(&self, snapshot_dir: &PathBuf) -> Result<(), String> { let manifest_path = snapshot_dir.join(MANIFEST_FILE); let data = self.to_bytes()?; std::fs::write(&manifest_path, data).map_err(|e| e.to_string()) } pub fn load(snapshot_dir: &PathBuf) -> Result { let manifest_path = snapshot_dir.join(MANIFEST_FILE); let data = std::fs::read(&manifest_path).map_err(|e| e.to_string())?; Self::from_bytes(&data) } } fn algorithm_name(compression: &VfsCompression) -> String { match compression { VfsCompression::None => "none".to_string(), VfsCompression::Lz4 => "lz4".to_string(), VfsCompression::Zstd => "zstd".to_string(), } } fn current_time_secs() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0) } #[derive(Debug, Clone)] pub struct BackupStream { pub format: SendFormat, pub manifest: BackupManifest, pub data: Vec, } impl BackupStream { pub fn new(format: SendFormat, manifest: BackupManifest, data: Vec) -> Self { Self { format, manifest, data } } pub fn to_bytes(&self) -> Result, String> { match self.format { SendFormat::CustomJson => { let manifest_bytes = self.manifest.to_bytes()?; let mut result = Vec::new(); result.extend_from_slice(&manifest_bytes.len().to_be_bytes()); result.extend_from_slice(&manifest_bytes); result.extend_from_slice(&self.data); Ok(result) } SendFormat::ZfsCompatible => { Err("ZFS compatible format not yet implemented".to_string()) } } } pub fn from_bytes(data: &[u8]) -> Result { if data.len() < 8 { return Err("Stream too short".to_string()); } let manifest_len = u64::from_be_bytes(data[0..8].try_into().map_err(|_| "Invalid length")?) as usize; if data.len() < 8 + manifest_len { return Err("Stream truncated".to_string()); } let manifest_bytes = &data[8..8 + manifest_len]; let manifest = BackupManifest::from_bytes(manifest_bytes)?; let payload = data[8 + manifest_len..].to_vec(); Ok(Self::new(manifest.format.clone(), manifest, payload)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_manifest_creation() { let manifest = BackupManifest::new("snap_2026-06-24".to_string(), PathBuf::from("/data")); assert_eq!(manifest.version, MANIFEST_VERSION); assert_eq!(manifest.format, SendFormat::CustomJson); assert_eq!(manifest.snapshot_name, "snap_2026-06-24"); } #[test] fn test_manifest_serialization() { let mut manifest = BackupManifest::new("test_snap".to_string(), PathBuf::from("/data")); manifest.add_file("file1.txt".to_string(), 1024, None); manifest.add_file("file2.txt".to_string(), 2048, None); manifest.calculate_ratio(); let bytes = manifest.to_bytes().unwrap(); let decoded = BackupManifest::from_bytes(&bytes).unwrap(); assert_eq!(decoded.files.len(), 2); assert_eq!(decoded.total_size, 3072); } #[test] fn test_backup_stream_roundtrip() { let manifest = BackupManifest::new("test".to_string(), PathBuf::from("/")); let stream = BackupStream::new(SendFormat::CustomJson, manifest, b"test data".to_vec()); let bytes = stream.to_bytes().unwrap(); let decoded = BackupStream::from_bytes(&bytes).unwrap(); assert_eq!(decoded.data, b"test data"); } #[test] fn test_compression_info() { let mut manifest = BackupManifest::new("test".to_string(), PathBuf::from("/")); manifest.set_compression(VfsCompression::Zstd, 1000, 420); assert!(manifest.compression.is_some()); let comp = manifest.compression.unwrap(); assert_eq!(comp.algorithm, "zstd"); assert_eq!(comp.ratio, 0.42); } #[test] fn test_encryption_info() { let mut manifest = BackupManifest::new("test".to_string(), PathBuf::from("/")); manifest.set_encryption(true, Some("key_hash_abc".to_string())); assert!(manifest.encryption.is_some()); let enc = manifest.encryption.unwrap(); assert!(enc.enabled); assert_eq!(enc.algorithm, "AES-256-GCM"); } }