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
267 lines
8.2 KiB
Rust
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");
|
|
}
|
|
} |