use anyhow::{Context, Result};
use std::path::PathBuf;
use crate::cache::storage::CacheStorage;
pub struct CacheTransaction<'a> {
storage: &'a CacheStorage,
crate_name: String,
version: String,
backup_path: Option<PathBuf>,
}
impl<'a> CacheTransaction<'a> {
pub fn new(storage: &'a CacheStorage, crate_name: &str, version: &str) -> Self {
Self {
storage,
crate_name: crate_name.to_string(),
version: version.to_string(),
backup_path: None,
}
}
pub fn begin(&mut self) -> Result<()> {
if self.storage.is_cached(&self.crate_name, &self.version) {
let backup_path = self
.storage
.backup_crate_to_temp(&self.crate_name, &self.version)
.context("Failed to create backup")?;
self.backup_path = Some(backup_path);
self.storage
.remove_crate(&self.crate_name, &self.version)
.context("Failed to remove existing cache")?;
}
Ok(())
}
pub fn commit(mut self) -> Result<()> {
if let Some(backup_path) = self.backup_path.take() {
let _ = self.storage.cleanup_backup(&backup_path);
}
Ok(())
}
pub fn rollback(&mut self) -> Result<()> {
if let Some(backup_path) = self.backup_path.take() {
if !backup_path.exists() {
anyhow::bail!(
"Backup path does not exist: {}. Cannot rollback.",
backup_path.display()
);
}
self.storage
.restore_crate_from_backup(&self.crate_name, &self.version, &backup_path)
.context("Failed to restore from backup")?;
let _ = self.storage.cleanup_backup(&backup_path);
}
Ok(())
}
}
impl<'a> Drop for CacheTransaction<'a> {
fn drop(&mut self) {
if self.backup_path.is_some() {
let _ = self.rollback();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_transaction_commit() -> Result<()> {
let temp_dir = TempDir::new()?;
let storage = CacheStorage::new(Some(temp_dir.path().to_path_buf()))?;
let source_path = storage.source_path("test-crate", "1.0.0")?;
storage.ensure_dir(&source_path)?;
fs::write(source_path.join("file.txt"), "original content")?;
storage.save_metadata("test-crate", "1.0.0")?;
let mut transaction = CacheTransaction::new(&storage, "test-crate", "1.0.0");
transaction.begin()?;
assert!(!storage.is_cached("test-crate", "1.0.0"));
let new_source_path = storage.source_path("test-crate", "1.0.0")?;
storage.ensure_dir(&new_source_path)?;
fs::write(new_source_path.join("file.txt"), "new content")?;
storage.save_metadata("test-crate", "1.0.0")?;
transaction.commit()?;
assert!(storage.is_cached("test-crate", "1.0.0"));
let content = fs::read_to_string(new_source_path.join("file.txt"))?;
assert_eq!(content, "new content");
Ok(())
}
#[test]
fn test_transaction_rollback() -> Result<()> {
let temp_dir = TempDir::new()?;
let storage = CacheStorage::new(Some(temp_dir.path().to_path_buf()))?;
let source_path = storage.source_path("test-crate", "1.0.0")?;
storage.ensure_dir(&source_path)?;
fs::write(source_path.join("file.txt"), "original content")?;
storage.save_metadata("test-crate", "1.0.0")?;
assert!(storage.is_cached("test-crate", "1.0.0"));
let mut transaction = CacheTransaction::new(&storage, "test-crate", "1.0.0");
transaction.begin()?;
assert!(!storage.is_cached("test-crate", "1.0.0"));
transaction.rollback()?;
assert!(storage.is_cached("test-crate", "1.0.0"));
let restored_source_path = storage.source_path("test-crate", "1.0.0")?;
let content = fs::read_to_string(restored_source_path.join("file.txt"))?;
assert_eq!(content, "original content");
Ok(())
}
}