Files
markbase/markbase-core/src/vfs/backup_manifest.rs
Warren 1418e9958b
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled
Apply clippy fixes for code quality
Clippy Fixes Applied:
- Removed unused imports
- Fixed manual implementation of .is_multiple_of()
- Fixed unnecessary_sort_by suggestions
- Added missing Ipv4Addr imports

Files Modified:
- forward_acl.rs: Add Ipv4Addr import
- known_hosts.rs: Add Ipv4Addr import
- Various files: Remove unused imports

Build:  markbase-core
Tests: 495 passed
2026-06-24 11:18:02 +08:00

267 lines
8.2 KiB
Rust

//! 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<VfsChecksumFile>,
pub dedup_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionInfo {
pub algorithm: String,
pub enabled: bool,
pub key_hash: Option<String>,
}
#[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<BackupFileEntry>,
pub dedup_manifest: Option<DedupManifest>,
pub encryption: Option<EncryptionInfo>,
pub compression: Option<CompressionInfo>,
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<VfsChecksumFile>) {
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<String>) {
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<Vec<u8>, String> {
serde_json::to_vec(self).map_err(|e| e.to_string())
}
pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
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<Self, String> {
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<u8>,
}
impl BackupStream {
pub fn new(format: SendFormat, manifest: BackupManifest, data: Vec<u8>) -> Self {
Self { format, manifest, data }
}
pub fn to_bytes(&self) -> Result<Vec<u8>, 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<Self, String> {
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");
}
}