use std::path::{Path, PathBuf};
use crate::types::{Layer2Error, Layer2Result};
const TEMP_FILE_PREFIX: &str = ".tmp_checkpoint_";
pub struct AtomicFileWriter {
sync_on_write: bool,
verify_write: bool,
}
impl AtomicFileWriter {
pub fn new() -> Self {
Self {
sync_on_write: true,
verify_write: true,
}
}
pub fn with_sync(mut self, sync: bool) -> Self {
self.sync_on_write = sync;
self
}
pub fn with_verify(mut self, verify: bool) -> Self {
self.verify_write = verify;
self
}
pub fn write_atomic(&self, filepath: &Path, content: &str) -> Layer2Result<()> {
let filepath = PathBuf::from(filepath);
let parent_dir = filepath.parent().ok_or_else(|| {
Layer2Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid file path",
))
})?;
std::fs::create_dir_all(parent_dir)?;
let temp_filename = format!("{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4());
let temp_path = parent_dir.join(temp_filename);
let result = self.do_write(&filepath, &temp_path, content);
if result.is_err() {
let _ = std::fs::remove_file(&temp_path);
}
result
}
fn do_write(&self, filepath: &Path, temp_path: &Path, content: &str) -> Layer2Result<()> {
use std::fs::File;
use std::io::Write;
{
let mut file = File::create(temp_path)?;
file.write_all(content.as_bytes())?;
if self.sync_on_write {
file.sync_all()?;
}
}
if self.verify_write {
let written = std::fs::read_to_string(temp_path)?;
if written != content {
return Err(Layer2Error::CheckpointCorrupted(
"Write verification failed: content mismatch".to_string(),
)
.into());
}
}
std::fs::rename(temp_path, filepath)?;
#[cfg(unix)]
if self.sync_on_write {
use std::os::unix::fs::OpenOptionsExt;
let dir_fd = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECTORY)
.open(filepath.parent().unwrap())?;
dir_fd.sync_all()?;
}
Ok(())
}
pub fn safe_remove(&self, path: &Path) -> Layer2Result<()> {
match std::fs::remove_file(path) {
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Layer2Error::Io(e).into()),
}
}
}
impl Default for AtomicFileWriter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_atomic_write() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.json");
let writer = AtomicFileWriter::new();
writer.write_atomic(&file_path, "test content").unwrap();
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "test content");
}
#[test]
fn test_atomic_write_creates_parent() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("nested/dir/test.json");
let writer = AtomicFileWriter::new();
writer.write_atomic(&file_path, "test").unwrap();
assert!(file_path.exists());
}
}