use chrono::{DateTime, Duration as ChronoDuration, Utc};
use hex;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::PathBuf;
use torsh_core::error::TorshError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackupStrategy {
Full,
Incremental,
Differential,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RetentionPolicy {
KeepDays(u32),
KeepLast(usize),
KeepAll,
Custom {
daily: u32,
weekly: u32,
monthly: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackupDestination {
Local(PathBuf),
S3 {
bucket: String,
region: String,
path: String,
},
Gcs {
bucket: String,
path: String,
},
Azure {
container: String,
path: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupConfig {
pub destination: PathBuf,
pub strategy: BackupStrategy,
pub compression: bool,
pub encryption: bool,
pub retention: RetentionPolicy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupMetadata {
pub backup_id: String,
pub package_id: String,
pub version: String,
pub strategy: BackupStrategy,
pub created_at: DateTime<Utc>,
pub size_bytes: u64,
pub compressed_size_bytes: Option<u64>,
pub checksum: String,
pub parent_backup_id: Option<String>,
pub compressed: bool,
pub encrypted: bool,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub backup_id: String,
pub success: bool,
pub checksum_valid: bool,
pub readable: bool,
pub size_valid: bool,
pub errors: Vec<String>,
pub verified_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryPoint {
pub id: String,
pub package_id: String,
pub version: String,
pub timestamp: DateTime<Utc>,
pub backup_chain: Vec<String>,
pub description: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BackupStatistics {
pub total_backups: usize,
pub full_backups: usize,
pub incremental_backups: usize,
pub differential_backups: usize,
pub total_storage_bytes: u64,
pub compressed_storage_bytes: u64,
pub compression_ratio: f64,
pub oldest_backup: Option<DateTime<Utc>>,
pub newest_backup: Option<DateTime<Utc>>,
pub failed_backups: usize,
}
pub struct BackupManager {
config: BackupConfig,
backups: HashMap<String, BackupMetadata>,
recovery_points: Vec<RecoveryPoint>,
statistics: BackupStatistics,
backup_data: HashMap<String, Vec<u8>>,
}
impl BackupManager {
pub fn new(config: BackupConfig) -> Self {
Self {
config,
backups: HashMap::new(),
recovery_points: Vec::new(),
statistics: BackupStatistics::default(),
backup_data: HashMap::new(),
}
}
pub fn create_backup(
&mut self,
package_id: &str,
version: &str,
data: &[u8],
) -> Result<String, TorshError> {
let backup_id = self.generate_backup_id(package_id, version);
let created_at = Utc::now();
let checksum = self.calculate_checksum(data);
let parent_backup_id = match self.config.strategy {
BackupStrategy::Full => None,
BackupStrategy::Incremental => self.get_last_backup_id(package_id, version),
BackupStrategy::Differential => self.get_last_full_backup_id(package_id, version),
};
let (final_data, compressed_size) = if self.config.compression {
let compressed = self.compress_data(data)?;
let size = compressed.len() as u64;
(compressed, Some(size))
} else {
(data.to_vec(), None)
};
let final_data = if self.config.encryption {
self.encrypt_data(&final_data)?
} else {
final_data
};
self.store_backup(&backup_id, &final_data)?;
let metadata = BackupMetadata {
backup_id: backup_id.clone(),
package_id: package_id.to_string(),
version: version.to_string(),
strategy: self.config.strategy,
created_at,
size_bytes: data.len() as u64,
compressed_size_bytes: compressed_size,
checksum,
parent_backup_id,
compressed: self.config.compression,
encrypted: self.config.encryption,
metadata: HashMap::new(),
};
self.backups.insert(backup_id.clone(), metadata);
self.update_statistics_after_backup();
self.apply_retention_policy()?;
Ok(backup_id)
}
pub fn restore_backup(&self, backup_id: &str) -> Result<Vec<u8>, TorshError> {
let metadata = self.backups.get(backup_id).ok_or_else(|| {
TorshError::InvalidArgument(format!("Backup {} not found", backup_id))
})?;
let mut data = self.load_backup(backup_id)?;
if metadata.encrypted {
data = self.decrypt_data(&data)?;
}
if metadata.compressed {
data = self.decompress_data(&data)?;
}
let checksum = self.calculate_checksum(&data);
if checksum != metadata.checksum {
return Err(TorshError::RuntimeError(
"Backup checksum mismatch".to_string(),
));
}
if let Some(parent_id) = &metadata.parent_backup_id {
let parent_data = self.restore_backup(parent_id)?;
data = self.merge_backup_data(&parent_data, &data)?;
}
Ok(data)
}
pub fn verify_backup(&self, backup_id: &str) -> VerificationResult {
let metadata = match self.backups.get(backup_id) {
Some(m) => m,
None => {
return VerificationResult {
backup_id: backup_id.to_string(),
success: false,
checksum_valid: false,
readable: false,
size_valid: false,
errors: vec!["Backup not found".to_string()],
verified_at: Utc::now(),
}
}
};
let mut errors = Vec::new();
let mut checksum_valid = false;
let mut readable = false;
let mut size_valid = false;
match self.load_backup(backup_id) {
Ok(data) => {
readable = true;
let expected_size = if metadata.compressed {
metadata
.compressed_size_bytes
.unwrap_or(metadata.size_bytes)
} else {
metadata.size_bytes
};
if data.len() as u64 == expected_size {
size_valid = true;
} else {
errors.push(format!(
"Size mismatch: expected {}, got {}",
expected_size,
data.len()
));
}
match self.restore_backup(backup_id) {
Ok(restored) => {
let checksum = self.calculate_checksum(&restored);
if checksum == metadata.checksum {
checksum_valid = true;
} else {
errors.push("Checksum mismatch".to_string());
}
}
Err(e) => {
errors.push(format!("Restoration failed: {}", e));
}
}
}
Err(e) => {
errors.push(format!("Failed to load backup: {}", e));
}
}
let success = errors.is_empty();
VerificationResult {
backup_id: backup_id.to_string(),
success,
checksum_valid,
readable,
size_valid,
errors,
verified_at: Utc::now(),
}
}
pub fn create_recovery_point(
&mut self,
package_id: &str,
version: &str,
description: String,
) -> Result<String, TorshError> {
let id = uuid::Uuid::new_v4().to_string();
let backup_chain = self.build_backup_chain(package_id, version)?;
let recovery_point = RecoveryPoint {
id: id.clone(),
package_id: package_id.to_string(),
version: version.to_string(),
timestamp: Utc::now(),
backup_chain,
description,
};
self.recovery_points.push(recovery_point);
Ok(id)
}
pub fn restore_to_recovery_point(
&self,
recovery_point_id: &str,
) -> Result<Vec<u8>, TorshError> {
let recovery_point = self
.recovery_points
.iter()
.find(|rp| rp.id == recovery_point_id)
.ok_or_else(|| {
TorshError::InvalidArgument(format!(
"Recovery point {} not found",
recovery_point_id
))
})?;
if let Some(last_backup) = recovery_point.backup_chain.last() {
self.restore_backup(last_backup)
} else {
Err(TorshError::InvalidArgument(
"Recovery point has no backups".to_string(),
))
}
}
pub fn list_backups(&self, package_id: &str) -> Vec<&BackupMetadata> {
self.backups
.values()
.filter(|m| m.package_id == package_id)
.collect()
}
pub fn get_statistics(&self) -> &BackupStatistics {
&self.statistics
}
pub fn delete_backup(&mut self, backup_id: &str) -> Result<(), TorshError> {
self.backups.remove(backup_id).ok_or_else(|| {
TorshError::InvalidArgument(format!("Backup {} not found", backup_id))
})?;
self.backup_data.remove(backup_id);
self.update_statistics();
Ok(())
}
pub fn apply_retention_policy(&mut self) -> Result<(), TorshError> {
let now = Utc::now();
let mut to_delete = Vec::new();
match self.config.retention {
RetentionPolicy::KeepDays(days) => {
let cutoff = now - ChronoDuration::days(days as i64);
for (id, metadata) in &self.backups {
if metadata.created_at < cutoff {
to_delete.push(id.clone());
}
}
}
RetentionPolicy::KeepLast(count) => {
let mut by_package: HashMap<String, Vec<&BackupMetadata>> = HashMap::new();
for metadata in self.backups.values() {
by_package
.entry(metadata.package_id.clone())
.or_insert_with(Vec::new)
.push(metadata);
}
for backups in by_package.values_mut() {
backups.sort_by(|a, b| b.created_at.cmp(&a.created_at));
for metadata in backups.iter().skip(count) {
to_delete.push(metadata.backup_id.clone());
}
}
}
RetentionPolicy::KeepAll => {
}
RetentionPolicy::Custom {
daily,
weekly,
monthly,
} => {
self.apply_gfs_retention(daily, weekly, monthly, &mut to_delete);
}
}
for backup_id in to_delete {
self.delete_backup(&backup_id)?;
}
Ok(())
}
fn generate_backup_id(&self, package_id: &str, version: &str) -> String {
format!(
"{}-{}-{}",
package_id,
version,
uuid::Uuid::new_v4().to_string()
)
}
fn calculate_checksum(&self, data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize().as_slice())
}
fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, TorshError> {
use oxiarc_deflate::gzip::gzip_compress;
gzip_compress(data, 6).map_err(|e| TorshError::RuntimeError(e.to_string()))
}
fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, TorshError> {
use oxiarc_deflate::gzip::gzip_decompress;
gzip_decompress(data).map_err(|e| TorshError::RuntimeError(e.to_string()))
}
fn encrypt_data(&self, data: &[u8]) -> Result<Vec<u8>, TorshError> {
Ok(data.to_vec())
}
fn decrypt_data(&self, data: &[u8]) -> Result<Vec<u8>, TorshError> {
Ok(data.to_vec())
}
fn store_backup(&mut self, backup_id: &str, data: &[u8]) -> Result<(), TorshError> {
self.backup_data
.insert(backup_id.to_string(), data.to_vec());
Ok(())
}
fn load_backup(&self, backup_id: &str) -> Result<Vec<u8>, TorshError> {
self.backup_data.get(backup_id).cloned().ok_or_else(|| {
TorshError::InvalidArgument(format!("Backup data {} not found", backup_id))
})
}
fn merge_backup_data(&self, _base: &[u8], delta: &[u8]) -> Result<Vec<u8>, TorshError> {
Ok(delta.to_vec())
}
fn get_last_backup_id(&self, package_id: &str, version: &str) -> Option<String> {
self.backups
.values()
.filter(|m| m.package_id == package_id && m.version == version)
.max_by_key(|m| m.created_at)
.map(|m| m.backup_id.clone())
}
fn get_last_full_backup_id(&self, package_id: &str, version: &str) -> Option<String> {
self.backups
.values()
.filter(|m| {
m.package_id == package_id
&& m.version == version
&& m.strategy == BackupStrategy::Full
})
.max_by_key(|m| m.created_at)
.map(|m| m.backup_id.clone())
}
fn build_backup_chain(
&self,
package_id: &str,
version: &str,
) -> Result<Vec<String>, TorshError> {
let mut chain = Vec::new();
if let Some(latest) = self
.backups
.values()
.filter(|m| m.package_id == package_id && m.version == version)
.max_by_key(|m| m.created_at)
{
chain.push(latest.backup_id.clone());
let mut current = latest;
while let Some(parent_id) = ¤t.parent_backup_id {
chain.push(parent_id.clone());
current = self.backups.get(parent_id).ok_or_else(|| {
TorshError::InvalidArgument(format!("Parent backup {} not found", parent_id))
})?;
}
}
chain.reverse();
Ok(chain)
}
fn update_statistics_after_backup(&mut self) {
self.update_statistics();
}
fn update_statistics(&mut self) {
let mut stats = BackupStatistics::default();
stats.total_backups = self.backups.len();
for metadata in self.backups.values() {
match metadata.strategy {
BackupStrategy::Full => stats.full_backups += 1,
BackupStrategy::Incremental => stats.incremental_backups += 1,
BackupStrategy::Differential => stats.differential_backups += 1,
}
stats.total_storage_bytes += metadata.size_bytes;
if let Some(compressed) = metadata.compressed_size_bytes {
stats.compressed_storage_bytes += compressed;
}
if stats.oldest_backup.is_none() || Some(metadata.created_at) < stats.oldest_backup {
stats.oldest_backup = Some(metadata.created_at);
}
if stats.newest_backup.is_none() || Some(metadata.created_at) > stats.newest_backup {
stats.newest_backup = Some(metadata.created_at);
}
}
if stats.total_storage_bytes > 0 {
stats.compression_ratio =
stats.compressed_storage_bytes as f64 / stats.total_storage_bytes as f64;
}
self.statistics = stats;
}
fn apply_gfs_retention(
&self,
_daily: u32,
_weekly: u32,
_monthly: u32,
_to_delete: &mut Vec<String>,
) {
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> BackupConfig {
BackupConfig {
destination: std::env::temp_dir().join("backups"),
strategy: BackupStrategy::Full,
compression: true,
encryption: false,
retention: RetentionPolicy::KeepLast(5),
}
}
#[test]
fn test_backup_manager_creation() {
let config = create_test_config();
let manager = BackupManager::new(config);
let stats = manager.get_statistics();
assert_eq!(stats.total_backups, 0);
}
#[test]
fn test_create_backup() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
let data = b"test package data";
let backup_id = manager.create_backup("test-pkg", "1.0.0", data).unwrap();
assert!(!backup_id.is_empty());
let stats = manager.get_statistics();
assert_eq!(stats.total_backups, 1);
assert_eq!(stats.full_backups, 1);
}
#[test]
fn test_restore_backup() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
let data = b"test package data";
let backup_id = manager.create_backup("test-pkg", "1.0.0", data).unwrap();
let restored = manager.restore_backup(&backup_id).unwrap();
assert_eq!(restored, data);
}
#[test]
fn test_list_backups() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
manager.create_backup("pkg1", "1.0.0", b"data1").unwrap();
manager.create_backup("pkg1", "2.0.0", b"data2").unwrap();
manager.create_backup("pkg2", "1.0.0", b"data3").unwrap();
let backups = manager.list_backups("pkg1");
assert_eq!(backups.len(), 2);
let backups = manager.list_backups("pkg2");
assert_eq!(backups.len(), 1);
}
#[test]
fn test_verify_backup() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
let data = b"test package data";
let backup_id = manager.create_backup("test-pkg", "1.0.0", data).unwrap();
let result = manager.verify_backup(&backup_id);
assert!(result.success);
assert!(result.readable);
}
#[test]
fn test_delete_backup() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
let backup_id = manager.create_backup("test-pkg", "1.0.0", b"data").unwrap();
assert_eq!(manager.get_statistics().total_backups, 1);
manager.delete_backup(&backup_id).unwrap();
assert_eq!(manager.get_statistics().total_backups, 0);
}
#[test]
fn test_retention_policy_keep_last() {
let mut config = create_test_config();
config.retention = RetentionPolicy::KeepLast(3);
let mut manager = BackupManager::new(config);
for i in 0..5 {
manager
.create_backup("test-pkg", "1.0.0", format!("data{}", i).as_bytes())
.unwrap();
}
assert_eq!(manager.get_statistics().total_backups, 3);
}
#[test]
fn test_incremental_backup() {
let mut config = create_test_config();
config.strategy = BackupStrategy::Incremental;
let mut manager = BackupManager::new(config);
let mut config2 = create_test_config();
config2.strategy = BackupStrategy::Full;
let mut manager2 = BackupManager::new(config2);
let full_id = manager2
.create_backup("test-pkg", "1.0.0", b"base data")
.unwrap();
if let Some(metadata) = manager2.backups.get(&full_id) {
manager.backups.insert(full_id.clone(), metadata.clone());
}
let inc_id = manager
.create_backup("test-pkg", "1.0.0", b"delta data")
.unwrap();
let metadata = manager.backups.get(&inc_id).unwrap();
assert_eq!(metadata.strategy, BackupStrategy::Incremental);
assert!(metadata.parent_backup_id.is_some());
}
#[test]
fn test_create_recovery_point() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
manager.create_backup("test-pkg", "1.0.0", b"data").unwrap();
let rp_id = manager
.create_recovery_point("test-pkg", "1.0.0", "Before update".to_string())
.unwrap();
assert!(!rp_id.is_empty());
assert_eq!(manager.recovery_points.len(), 1);
}
#[test]
fn test_backup_statistics() {
let config = create_test_config();
let mut manager = BackupManager::new(config);
let data = b"test data with some content";
manager.create_backup("pkg1", "1.0.0", data).unwrap();
manager.create_backup("pkg2", "1.0.0", data).unwrap();
let stats = manager.get_statistics();
assert_eq!(stats.total_backups, 2);
assert_eq!(stats.full_backups, 2);
assert!(stats.total_storage_bytes > 0);
assert!(stats.newest_backup.is_some());
assert!(stats.oldest_backup.is_some());
}
}