use crate::utils::error::{Error, Result};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use tempfile::NamedTempFile;
#[derive(Debug, Clone)]
enum FileOperation {
Created { path: PathBuf },
Modified { path: PathBuf, backup: PathBuf },
}
#[derive(Debug)]
pub struct FileTransaction {
operations: Mutex<Vec<FileOperation>>,
backup_dir: Option<PathBuf>,
committed: AtomicBool,
}
impl FileTransaction {
pub fn new() -> Result<Self> {
Ok(Self {
operations: Mutex::new(Vec::new()),
backup_dir: None,
committed: AtomicBool::new(false),
})
}
pub fn with_backup_dir(backup_dir: impl AsRef<Path>) -> Result<Self> {
let backup_path = backup_dir.as_ref().to_path_buf();
fs::create_dir_all(&backup_path).map_err(|e| {
Error::new(&format!(
"Failed to create backup directory {}: {}",
backup_path.display(),
e
))
})?;
Ok(Self {
operations: Mutex::new(Vec::new()),
backup_dir: Some(backup_path),
committed: AtomicBool::new(false),
})
}
pub fn write_file(&self, path: impl AsRef<Path>, content: &str) -> Result<()> {
let path = path.as_ref();
let existed = path.exists();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
Error::new(&format!(
"Failed to create parent directory {}: {}",
parent.display(),
e
))
})?;
}
let backup_path = if existed {
let backup = self.create_backup(path)?;
Some(backup)
} else {
None
};
let temp_dir = path.parent().unwrap_or_else(|| Path::new("."));
let mut temp_file = NamedTempFile::new_in(temp_dir).map_err(|e| {
Error::new(&format!(
"Failed to create temporary file in {}: {}",
temp_dir.display(),
e
))
})?;
use std::io::Write;
temp_file
.write_all(content.as_bytes())
.map_err(|e| Error::new(&format!("Failed to write to temporary file: {}", e)))?;
temp_file.persist(path).map_err(|e| {
Error::new(&format!(
"Failed to atomically write to {}: {}",
path.display(),
e
))
})?;
let operation = if let Some(backup) = backup_path {
FileOperation::Modified {
path: path.to_path_buf(),
backup,
}
} else {
FileOperation::Created {
path: path.to_path_buf(),
}
};
self.operations.lock().push(operation);
Ok(())
}
fn create_backup(&self, path: &Path) -> Result<PathBuf> {
let backup_path = if let Some(backup_dir) = &self.backup_dir {
let filename = path
.file_name()
.ok_or_else(|| Error::new(&format!("Invalid path: {}", path.display())))?;
backup_dir.join(format!(
"{}.backup.{}",
filename.to_string_lossy(),
chrono::Utc::now().timestamp()
))
} else {
path.with_extension(format!(
"{}.backup",
path.extension().and_then(|e| e.to_str()).unwrap_or("txt")
))
};
fs::copy(path, &backup_path).map_err(|e| {
Error::new(&format!(
"Failed to create backup of {} to {}: {}",
path.display(),
backup_path.display(),
e
))
})?;
Ok(backup_path)
}
pub fn commit(self) -> Result<TransactionReceipt> {
self.committed.store(true, Ordering::SeqCst);
let operations = self.operations.lock();
let receipt = TransactionReceipt {
files_created: operations
.iter()
.filter_map(|op| match op {
FileOperation::Created { path } => Some(path.clone()),
FileOperation::Modified { .. } => None,
})
.collect(),
files_modified: operations
.iter()
.filter_map(|op| match op {
FileOperation::Modified { path, .. } => Some(path.clone()),
FileOperation::Created { .. } => None,
})
.collect(),
backups: operations
.iter()
.filter_map(|op| match op {
FileOperation::Modified { path, backup } => {
Some((path.clone(), backup.clone()))
}
FileOperation::Created { .. } => None,
})
.collect(),
};
Ok(receipt)
}
fn rollback(&self) {
if self.committed.load(Ordering::SeqCst) {
return;
}
let operations = self.operations.lock();
for operation in operations.iter().rev() {
match operation {
FileOperation::Created { path } => {
if let Err(e) = fs::remove_file(path) {
eprintln!(
"Warning: Failed to remove {} during rollback: {}",
path.display(),
e
);
}
}
FileOperation::Modified { path, backup } => {
if let Err(e) = fs::copy(backup, path) {
eprintln!(
"Warning: Failed to restore {} from backup during rollback: {}",
path.display(),
e
);
}
let _ = fs::remove_file(backup);
}
}
}
}
}
impl Drop for FileTransaction {
fn drop(&mut self) {
if !self.committed.load(Ordering::SeqCst) {
self.rollback();
}
}
}
#[derive(Debug, Clone)]
pub struct TransactionReceipt {
pub files_created: Vec<PathBuf>,
pub files_modified: Vec<PathBuf>,
pub backups: HashMap<PathBuf, PathBuf>,
}
impl TransactionReceipt {
pub fn clean_backups(&self) -> Result<()> {
for backup in self.backups.values() {
if let Err(e) = fs::remove_file(backup) {
eprintln!(
"Warning: Failed to remove backup {}: {}",
backup.display(),
e
);
}
}
Ok(())
}
pub fn total_files(&self) -> usize {
self.files_created.len() + self.files_modified.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_atomic_write_new_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let tx = FileTransaction::new().unwrap();
tx.write_file(&file_path, "test content").unwrap();
assert!(file_path.exists());
assert_eq!(fs::read_to_string(&file_path).unwrap(), "test content");
let receipt = tx.commit().unwrap();
assert_eq!(receipt.files_created.len(), 1);
assert_eq!(receipt.files_modified.len(), 0);
}
#[test]
fn test_atomic_write_existing_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "original").unwrap();
let tx = FileTransaction::new().unwrap();
tx.write_file(&file_path, "modified").unwrap();
assert_eq!(fs::read_to_string(&file_path).unwrap(), "modified");
let receipt = tx.commit().unwrap();
assert_eq!(receipt.files_created.len(), 0);
assert_eq!(receipt.files_modified.len(), 1);
assert_eq!(receipt.backups.len(), 1);
}
#[test]
fn test_rollback_on_drop() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
{
let tx = FileTransaction::new().unwrap();
tx.write_file(&file_path, "test content").unwrap();
assert!(file_path.exists());
}
assert!(!file_path.exists());
}
#[test]
fn test_rollback_restores_original() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "original").unwrap();
{
let tx = FileTransaction::new().unwrap();
tx.write_file(&file_path, "modified").unwrap();
assert_eq!(fs::read_to_string(&file_path).unwrap(), "modified");
}
assert_eq!(fs::read_to_string(&file_path).unwrap(), "original");
}
#[test]
fn test_multiple_operations_rollback() {
let dir = tempdir().unwrap();
let file1 = dir.path().join("file1.txt");
let file2 = dir.path().join("file2.txt");
{
let tx = FileTransaction::new().unwrap();
tx.write_file(&file1, "content1").unwrap();
tx.write_file(&file2, "content2").unwrap();
assert!(file1.exists());
assert!(file2.exists());
}
assert!(!file1.exists());
assert!(!file2.exists());
}
}