Fix Backup/Restore API compilation errors
- chrono timestamp_opt API: use TimeZone trait method - VfsError::Io/NotFound: use String literals - SendFormat: add PartialEq derive - VfsRaidConfig tests: add disk_paths field - BackupStats test: use relative timestamps - HashSet file tracking: use (String, u64) tuple - BackupStream::receive: clone format before use - collect_file_data: fix temporary lifetime All tests pass: 495 markbase-core + 201 smb-server = 696 total
This commit is contained in:
268
markbase-core/src/vfs/backup_manifest.rs
Normal file
268
markbase-core/src/vfs/backup_manifest.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
//! Backup Manifest - Snapshot metadata serialization
|
||||
//!
|
||||
//! Compatible with ZFS send/receive and Proxmox Backup Server format
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sha2::{Sha256, 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user