use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tracing::{debug, error, info, warn};
const DEFAULT_BACKUP_RETENTION_HOURS: u64 = 24;
const LOCK_TIMEOUT_SECS: u64 = 30;
const BACKUP_DIR_NAME: &str = ".fstar-lint-backups";
#[derive(Debug)]
pub enum AtomicWriteError {
TempFileCreation { path: PathBuf, source: io::Error },
TempFileWrite { path: PathBuf, source: io::Error },
TempFileSync { path: PathBuf, source: io::Error },
Rename {
from: PathBuf,
to: PathBuf,
source: io::Error,
},
BackupCreation { path: PathBuf, source: io::Error },
OriginalRead { path: PathBuf, source: io::Error },
BackupDirCreation { path: PathBuf, source: io::Error },
LockAcquisitionFailed { path: PathBuf, reason: String },
StaleLockRemoval { path: PathBuf, source: io::Error },
LockReleaseFailed { path: PathBuf, source: io::Error },
RollbackFailed { path: PathBuf, source: io::Error },
ValidationFailed {
path: PathBuf,
expected_len: usize,
actual_len: usize,
},
ParentDirMissing { path: PathBuf },
CrossFilesystem { path: PathBuf },
}
impl std::fmt::Display for AtomicWriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AtomicWriteError::TempFileCreation { path, source } => {
write!(
f,
"Failed to create temporary file at {}: {}",
path.display(),
source
)
}
AtomicWriteError::TempFileWrite { path, source } => {
write!(
f,
"Failed to write to temporary file {}: {}",
path.display(),
source
)
}
AtomicWriteError::TempFileSync { path, source } => {
write!(
f,
"Failed to sync temporary file {}: {}",
path.display(),
source
)
}
AtomicWriteError::Rename { from, to, source } => {
write!(
f,
"Failed to rename {} to {}: {}",
from.display(),
to.display(),
source
)
}
AtomicWriteError::BackupCreation { path, source } => {
write!(f, "Failed to create backup at {}: {}", path.display(), source)
}
AtomicWriteError::OriginalRead { path, source } => {
write!(
f,
"Failed to read original file {}: {}",
path.display(),
source
)
}
AtomicWriteError::BackupDirCreation { path, source } => {
write!(
f,
"Failed to create backup directory {}: {}",
path.display(),
source
)
}
AtomicWriteError::LockAcquisitionFailed { path, reason } => {
write!(
f,
"Failed to acquire lock for {}: {}",
path.display(),
reason
)
}
AtomicWriteError::StaleLockRemoval { path, source } => {
write!(
f,
"Failed to remove stale lock file {}: {}",
path.display(),
source
)
}
AtomicWriteError::LockReleaseFailed { path, source } => {
write!(f, "Failed to release lock for {}: {}", path.display(), source)
}
AtomicWriteError::RollbackFailed { path, source } => {
write!(f, "Failed to rollback {}: {}", path.display(), source)
}
AtomicWriteError::ValidationFailed {
path,
expected_len,
actual_len,
} => {
write!(
f,
"Content validation failed for {}: expected {} bytes, got {}",
path.display(),
expected_len,
actual_len
)
}
AtomicWriteError::ParentDirMissing { path } => {
write!(
f,
"Parent directory does not exist for {}",
path.display()
)
}
AtomicWriteError::CrossFilesystem { path } => {
write!(
f,
"File {} is on different filesystem, atomic rename not possible",
path.display()
)
}
}
}
}
impl std::error::Error for AtomicWriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AtomicWriteError::TempFileCreation { source, .. } => Some(source),
AtomicWriteError::TempFileWrite { source, .. } => Some(source),
AtomicWriteError::TempFileSync { source, .. } => Some(source),
AtomicWriteError::Rename { source, .. } => Some(source),
AtomicWriteError::BackupCreation { source, .. } => Some(source),
AtomicWriteError::OriginalRead { source, .. } => Some(source),
AtomicWriteError::BackupDirCreation { source, .. } => Some(source),
AtomicWriteError::StaleLockRemoval { source, .. } => Some(source),
AtomicWriteError::LockReleaseFailed { source, .. } => Some(source),
AtomicWriteError::RollbackFailed { source, .. } => Some(source),
_ => None,
}
}
}
pub type AtomicWriteResult<T> = Result<T, AtomicWriteError>;
#[derive(Debug, Clone)]
pub struct BackupInfo {
pub backup_path: PathBuf,
pub original_path: PathBuf,
pub created_at: SystemTime,
pub size: u64,
}
pub struct AtomicWriter {
active_locks: Arc<Mutex<HashMap<PathBuf, PathBuf>>>,
backup_retention: Duration,
validate_writes: bool,
}
impl Default for AtomicWriter {
fn default() -> Self {
Self::new()
}
}
impl AtomicWriter {
pub fn new() -> Self {
Self {
active_locks: Arc::new(Mutex::new(HashMap::new())),
backup_retention: Duration::from_secs(DEFAULT_BACKUP_RETENTION_HOURS * 3600),
validate_writes: true,
}
}
pub fn with_retention(retention_hours: u64) -> Self {
Self {
active_locks: Arc::new(Mutex::new(HashMap::new())),
backup_retention: Duration::from_secs(retention_hours * 3600),
validate_writes: true,
}
}
pub fn without_validation(mut self) -> Self {
self.validate_writes = false;
self
}
pub fn write(&self, path: &Path, content: &str) -> AtomicWriteResult<()> {
let parent = path.parent().ok_or_else(|| AtomicWriteError::ParentDirMissing {
path: path.to_path_buf(),
})?;
if !parent.exists() {
return Err(AtomicWriteError::ParentDirMissing {
path: path.to_path_buf(),
});
}
let temp_path = self.generate_temp_path(path);
debug!("Creating temp file: {}", temp_path.display());
let mut temp_file = File::create(&temp_path).map_err(|e| AtomicWriteError::TempFileCreation {
path: temp_path.clone(),
source: e,
})?;
temp_file
.write_all(content.as_bytes())
.map_err(|e| AtomicWriteError::TempFileWrite {
path: temp_path.clone(),
source: e,
})?;
temp_file
.sync_all()
.map_err(|e| AtomicWriteError::TempFileSync {
path: temp_path.clone(),
source: e,
})?;
drop(temp_file);
fs::rename(&temp_path, path).map_err(|e| {
let _ = fs::remove_file(&temp_path);
AtomicWriteError::Rename {
from: temp_path.clone(),
to: path.to_path_buf(),
source: e,
}
})?;
if self.validate_writes {
self.validate_content(path, content)?;
}
debug!("Successfully wrote {} bytes to {}", content.len(), path.display());
Ok(())
}
pub fn write_with_backup(&self, path: &Path, content: &str) -> AtomicWriteResult<PathBuf> {
self.acquire_lock(path)?;
let backup_path = if path.exists() {
Some(self.create_backup(path)?)
} else {
None
};
let write_result = self.write(path, content);
let release_result = self.release_lock(path);
if let Err(write_err) = write_result {
if let Some(ref backup) = backup_path {
warn!("Write failed, attempting rollback from backup");
if let Err(rollback_err) = self.rollback(path, backup) {
error!(
"CRITICAL: Write failed AND rollback failed! Backup at: {}",
backup.display()
);
return Err(write_err);
}
info!("Successfully rolled back from backup");
}
return Err(write_err);
}
if let Err(lock_err) = release_result {
warn!("Lock release failed (but write succeeded): {:?}", lock_err);
}
Ok(backup_path.unwrap_or_else(|| self.generate_backup_path(path)))
}
pub fn rollback(&self, original: &Path, backup: &Path) -> AtomicWriteResult<()> {
if !backup.exists() {
warn!("Backup file does not exist: {}", backup.display());
return Ok(());
}
let mut backup_content = String::new();
File::open(backup)
.and_then(|mut f| f.read_to_string(&mut backup_content))
.map_err(|e| AtomicWriteError::RollbackFailed {
path: original.to_path_buf(),
source: e,
})?;
self.write(original, &backup_content)?;
info!(
"Rolled back {} from backup {}",
original.display(),
backup.display()
);
Ok(())
}
fn create_backup(&self, path: &Path) -> AtomicWriteResult<PathBuf> {
let backup_dir = self.get_backup_dir(path)?;
let backup_path = self.generate_backup_path(path);
let mut content = String::new();
File::open(path)
.and_then(|mut f| f.read_to_string(&mut content))
.map_err(|e| AtomicWriteError::OriginalRead {
path: path.to_path_buf(),
source: e,
})?;
if !backup_dir.exists() {
fs::create_dir_all(&backup_dir).map_err(|e| AtomicWriteError::BackupDirCreation {
path: backup_dir.clone(),
source: e,
})?;
}
let mut backup_file =
File::create(&backup_path).map_err(|e| AtomicWriteError::BackupCreation {
path: backup_path.clone(),
source: e,
})?;
backup_file
.write_all(content.as_bytes())
.map_err(|e| AtomicWriteError::BackupCreation {
path: backup_path.clone(),
source: e,
})?;
backup_file
.sync_all()
.map_err(|e| AtomicWriteError::BackupCreation {
path: backup_path.clone(),
source: e,
})?;
info!("Created backup: {}", backup_path.display());
Ok(backup_path)
}
fn acquire_lock(&self, path: &Path) -> AtomicWriteResult<()> {
let lock_path = self.get_lock_path(path);
if lock_path.exists() {
if self.is_lock_stale(&lock_path)? {
info!("Removing stale lock: {}", lock_path.display());
fs::remove_file(&lock_path).map_err(|e| AtomicWriteError::StaleLockRemoval {
path: lock_path.clone(),
source: e,
})?;
} else {
return Err(AtomicWriteError::LockAcquisitionFailed {
path: path.to_path_buf(),
reason: format!(
"File is locked by another process (lock file: {})",
lock_path.display()
),
});
}
}
let lock_content = format!(
"pid:{}\ntime:{}\nfile:{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
path.display()
);
if let Some(parent) = lock_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| AtomicWriteError::BackupDirCreation {
path: parent.to_path_buf(),
source: e,
})?;
}
}
let mut lock_file = OpenOptions::new()
.write(true)
.create_new(true) .open(&lock_path)
.map_err(|e| {
if e.kind() == io::ErrorKind::AlreadyExists {
AtomicWriteError::LockAcquisitionFailed {
path: path.to_path_buf(),
reason: "Lock file was created by another process".to_string(),
}
} else {
AtomicWriteError::LockAcquisitionFailed {
path: path.to_path_buf(),
reason: format!("Failed to create lock file: {}", e),
}
}
})?;
lock_file.write_all(lock_content.as_bytes()).map_err(|e| {
let _ = fs::remove_file(&lock_path);
AtomicWriteError::LockAcquisitionFailed {
path: path.to_path_buf(),
reason: format!("Failed to write lock file content: {}", e),
}
})?;
let mut locks = self.active_locks.lock().unwrap();
locks.insert(path.to_path_buf(), lock_path.clone());
debug!("Acquired lock: {}", lock_path.display());
Ok(())
}
fn release_lock(&self, path: &Path) -> AtomicWriteResult<()> {
let lock_path = self.get_lock_path(path);
{
let mut locks = self.active_locks.lock().unwrap();
locks.remove(path);
}
if lock_path.exists() {
fs::remove_file(&lock_path).map_err(|e| AtomicWriteError::LockReleaseFailed {
path: path.to_path_buf(),
source: e,
})?;
}
debug!("Released lock: {}", lock_path.display());
Ok(())
}
fn is_lock_stale(&self, lock_path: &Path) -> AtomicWriteResult<bool> {
let metadata = fs::metadata(lock_path).map_err(|e| AtomicWriteError::LockAcquisitionFailed {
path: lock_path.to_path_buf(),
reason: format!("Cannot read lock file metadata: {}", e),
})?;
let modified = metadata.modified().map_err(|e| {
AtomicWriteError::LockAcquisitionFailed {
path: lock_path.to_path_buf(),
reason: format!("Cannot read lock file mtime: {}", e),
}
})?;
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or(Duration::ZERO);
Ok(age.as_secs() > LOCK_TIMEOUT_SECS)
}
fn validate_content(&self, path: &Path, expected: &str) -> AtomicWriteResult<()> {
let mut actual = String::new();
File::open(path)
.and_then(|mut f| f.read_to_string(&mut actual))
.map_err(|e| AtomicWriteError::OriginalRead {
path: path.to_path_buf(),
source: e,
})?;
if actual.len() != expected.len() {
return Err(AtomicWriteError::ValidationFailed {
path: path.to_path_buf(),
expected_len: expected.len(),
actual_len: actual.len(),
});
}
if actual != expected {
return Err(AtomicWriteError::ValidationFailed {
path: path.to_path_buf(),
expected_len: expected.len(),
actual_len: actual.len(),
});
}
Ok(())
}
fn generate_temp_path(&self, path: &Path) -> PathBuf {
let parent = path.parent().unwrap_or(Path::new("."));
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file");
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
parent.join(format!(".{}.{}.tmp", filename, timestamp))
}
fn generate_backup_path(&self, path: &Path) -> PathBuf {
let backup_dir = self.get_backup_dir(path).unwrap_or_else(|_| {
path.parent()
.unwrap_or(Path::new("."))
.join(BACKUP_DIR_NAME)
});
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file");
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
backup_dir.join(format!("{}.{}.bak", filename, timestamp))
}
fn get_backup_dir(&self, path: &Path) -> AtomicWriteResult<PathBuf> {
let parent = path.parent().ok_or_else(|| AtomicWriteError::ParentDirMissing {
path: path.to_path_buf(),
})?;
Ok(parent.join(BACKUP_DIR_NAME))
}
fn get_lock_path(&self, path: &Path) -> PathBuf {
let backup_dir = self
.get_backup_dir(path)
.unwrap_or_else(|_| path.parent().unwrap_or(Path::new(".")).join(BACKUP_DIR_NAME));
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file");
backup_dir.join(format!("{}.lock", filename))
}
pub fn list_backups(&self, dir: &Path) -> AtomicWriteResult<Vec<BackupInfo>> {
let backup_dir = dir.join(BACKUP_DIR_NAME);
if !backup_dir.exists() {
return Ok(Vec::new());
}
let mut backups = Vec::new();
let entries = fs::read_dir(&backup_dir).map_err(|e| AtomicWriteError::BackupDirCreation {
path: backup_dir.clone(),
source: e,
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("bak") {
if let Ok(metadata) = fs::metadata(&path) {
let created_at = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let original_name: String = filename
.rsplitn(3, '.')
.skip(2)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join(".");
let original_path = dir.join(&original_name);
backups.push(BackupInfo {
backup_path: path,
original_path,
created_at,
size: metadata.len(),
});
}
}
}
backups.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(backups)
}
pub fn cleanup_old_backups(&self, dir: &Path) -> AtomicWriteResult<usize> {
let backups = self.list_backups(dir)?;
let now = SystemTime::now();
let mut removed = 0;
for backup in backups {
let age = now.duration_since(backup.created_at).unwrap_or(Duration::ZERO);
if age > self.backup_retention {
if let Err(e) = fs::remove_file(&backup.backup_path) {
warn!(
"Failed to remove old backup {}: {}",
backup.backup_path.display(),
e
);
} else {
debug!("Removed old backup: {}", backup.backup_path.display());
removed += 1;
}
}
}
let backup_dir = dir.join(BACKUP_DIR_NAME);
if backup_dir.exists() {
if let Ok(entries) = fs::read_dir(&backup_dir) {
let file_count = entries
.flatten()
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext != "lock")
.unwrap_or(true)
})
.count();
if file_count == 0 {
let _ = fs::remove_dir(&backup_dir);
}
}
}
if removed > 0 {
info!("Cleaned up {} old backup(s) from {}", removed, dir.display());
}
Ok(removed)
}
pub fn restore_backup(&self, backup: &BackupInfo) -> AtomicWriteResult<()> {
if !backup.backup_path.exists() {
return Err(AtomicWriteError::RollbackFailed {
path: backup.original_path.clone(),
source: io::Error::new(io::ErrorKind::NotFound, "Backup file not found"),
});
}
self.rollback(&backup.original_path, &backup.backup_path)
}
}
impl Drop for AtomicWriter {
fn drop(&mut self) {
let locks = self.active_locks.lock().unwrap();
for (path, lock_path) in locks.iter() {
if lock_path.exists() {
if let Err(e) = fs::remove_file(lock_path) {
error!(
"Failed to release lock for {} on drop: {}",
path.display(),
e
);
}
}
}
}
}
pub fn atomic_write(path: &Path, content: &str) -> AtomicWriteResult<()> {
AtomicWriter::new().write(path, content)
}
pub fn atomic_write_with_backup(path: &Path, content: &str) -> AtomicWriteResult<PathBuf> {
AtomicWriter::new().write_with_backup(path, content)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
fs::write(&path, content).expect("Failed to write test file");
path
}
#[test]
fn test_atomic_write_success() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("test.txt");
let writer = AtomicWriter::new();
writer
.write(&file_path, "Hello, World!")
.expect("Atomic write should succeed");
let content = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(content, "Hello, World!");
}
#[test]
fn test_atomic_write_overwrites_existing() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.txt", "Original content");
let writer = AtomicWriter::new();
writer
.write(&file_path, "New content")
.expect("Atomic write should succeed");
let content = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(content, "New content");
}
#[test]
fn test_atomic_write_with_backup_creates_backup() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Original F* content");
let writer = AtomicWriter::new();
let backup_path = writer
.write_with_backup(&file_path, "Modified F* content")
.expect("Atomic write with backup should succeed");
let content = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(content, "Modified F* content");
assert!(backup_path.exists(), "Backup file should exist");
let backup_content = fs::read_to_string(&backup_path).expect("Should read backup");
assert_eq!(backup_content, "Original F* content");
}
#[test]
fn test_rollback_restores_original() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Original content");
let writer = AtomicWriter::new();
let backup_path = writer
.write_with_backup(&file_path, "Modified content")
.expect("Write with backup should succeed");
let content = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(content, "Modified content");
writer
.rollback(&file_path, &backup_path)
.expect("Rollback should succeed");
let restored = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(restored, "Original content");
}
#[test]
fn test_lock_prevents_concurrent_access() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Content");
let writer1 = AtomicWriter::new();
let writer2 = AtomicWriter::new();
writer1
.acquire_lock(&file_path)
.expect("First lock should succeed");
let result = writer2.acquire_lock(&file_path);
assert!(result.is_err(), "Second lock should fail");
writer1
.release_lock(&file_path)
.expect("Lock release should succeed");
writer2
.acquire_lock(&file_path)
.expect("Second lock should succeed after release");
writer2
.release_lock(&file_path)
.expect("Cleanup lock release");
}
#[test]
fn test_list_backups() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Content v1");
let writer = AtomicWriter::new();
writer
.write_with_backup(&file_path, "Content v2")
.expect("First backup should succeed");
std::thread::sleep(std::time::Duration::from_millis(100));
writer
.write_with_backup(&file_path, "Content v3")
.expect("Second backup should succeed");
let backups = writer
.list_backups(temp_dir.path())
.expect("List backups should succeed");
assert!(backups.len() >= 2, "Should have at least 2 backups");
}
#[test]
fn test_cleanup_old_backups() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Content");
let writer = AtomicWriter {
active_locks: Arc::new(Mutex::new(HashMap::new())),
backup_retention: Duration::from_secs(1),
validate_writes: true,
};
writer
.write_with_backup(&file_path, "New content")
.expect("Backup should succeed");
std::thread::sleep(std::time::Duration::from_secs(2));
let removed = writer
.cleanup_old_backups(temp_dir.path())
.expect("Cleanup should succeed");
assert!(removed > 0, "Should have removed at least one old backup");
}
#[test]
fn test_temp_file_cleanup_on_failure() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("nonexistent_dir").join("test.txt");
let writer = AtomicWriter::new();
let result = writer.write(&file_path, "Content");
assert!(result.is_err());
let temp_files: Vec<_> = fs::read_dir(temp_dir.path())
.expect("Should read dir")
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == "tmp")
.unwrap_or(false)
})
.collect();
assert!(temp_files.is_empty(), "No temp files should be left behind");
}
#[test]
fn test_validation_catches_corruption() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("test.txt");
let writer = AtomicWriter::new();
writer
.write(&file_path, "Test content")
.expect("Write should succeed");
let content = fs::read_to_string(&file_path).expect("Read should succeed");
assert_eq!(content, "Test content");
}
#[test]
fn test_write_new_file_without_backup() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("new_file.fst");
let writer = AtomicWriter::new();
let result = writer.write_with_backup(&file_path, "New file content");
assert!(result.is_ok(), "Write should succeed for new file");
let content = fs::read_to_string(&file_path).expect("Should read file");
assert_eq!(content, "New file content");
}
#[test]
fn test_convenience_functions() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file1 = temp_dir.path().join("file1.txt");
atomic_write(&file1, "Content 1").expect("atomic_write should succeed");
assert_eq!(
fs::read_to_string(&file1).unwrap(),
"Content 1"
);
let file2 = temp_dir.path().join("file2.txt");
fs::write(&file2, "Original").expect("Setup file");
let backup = atomic_write_with_backup(&file2, "Content 2")
.expect("atomic_write_with_backup should succeed");
assert_eq!(
fs::read_to_string(&file2).unwrap(),
"Content 2"
);
assert!(backup.exists() || !temp_dir.path().join(BACKUP_DIR_NAME).exists());
}
#[test]
fn test_restore_backup() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_test_file(&temp_dir, "test.fst", "Original content");
let writer = AtomicWriter::new();
writer
.write_with_backup(&file_path, "Modified content")
.expect("Write should succeed");
let backups = writer
.list_backups(temp_dir.path())
.expect("List should succeed");
assert!(!backups.is_empty(), "Should have backups");
writer
.restore_backup(&backups[0])
.expect("Restore should succeed");
let content = fs::read_to_string(&file_path).expect("Read should succeed");
assert_eq!(content, "Original content");
}
}