use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use crate::utils::error::{Error, Result};
const MIN_FREE_SPACE: u64 = 10 * 1024 * 1024;
pub struct AtomicFileWriter<State = Uncommitted> {
target_path: PathBuf,
temp_path: PathBuf,
file: File,
committed: bool, _state: PhantomData<State>,
}
#[derive(Debug)]
pub struct Uncommitted;
#[derive(Debug)]
pub struct Committed;
impl AtomicFileWriter<Uncommitted> {
pub fn new(target: impl AsRef<Path>) -> Result<Self> {
let target_path = target.as_ref().to_path_buf();
Self::check_disk_space(&target_path)?;
if let Some(parent) = target_path.parent() {
if !parent.exists() {
return Err(Error::invalid_input(format!(
"Parent directory does not exist: {}",
parent.display()
)));
}
}
let temp_path = Self::temp_path(&target_path);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&temp_path)
.map_err(|e| Error::io_error(format!("Failed to create temp file: {}", e)))?;
Ok(Self {
target_path,
temp_path,
file,
committed: false,
_state: PhantomData,
})
}
pub fn write_all(&mut self, data: &[u8]) -> Result<()> {
self.file
.write_all(data)
.map_err(|e| Error::io_error(format!("Failed to write data: {}", e)))?;
self.file
.sync_all()
.map_err(|e| Error::io_error(format!("Failed to sync data: {}", e)))?;
Ok(())
}
pub fn commit(mut self) -> Result<AtomicFileWriter<Committed>> {
fs::rename(&self.temp_path, &self.target_path).map_err(|e| {
Error::io_error(format!("Failed to commit file (rename failed): {}", e))
})?;
self.committed = true;
Ok(AtomicFileWriter {
target_path: self.target_path.clone(),
temp_path: self.temp_path.clone(),
file: self
.file
.try_clone()
.map_err(|e| Error::io_error(format!("Failed to clone file handle: {}", e)))?,
committed: true,
_state: PhantomData,
})
}
fn temp_path(target: &Path) -> PathBuf {
let mut temp = target.as_os_str().to_os_string();
temp.push(".tmp");
PathBuf::from(temp)
}
fn check_disk_space(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let Some(parent) = path.parent() {
if parent.exists() {
if let Ok(metadata) = fs::metadata(parent) {
let block_size = metadata.blksize();
let blocks_avail = metadata.blocks();
let available = block_size * blocks_avail;
if available < MIN_FREE_SPACE {
return Err(Error::io_error(format!(
"Insufficient disk space: {} bytes available, {} required",
available, MIN_FREE_SPACE
)));
}
}
}
}
}
#[cfg(not(unix))]
{
let _ = path; }
Ok(())
}
}
impl<State> Drop for AtomicFileWriter<State> {
fn drop(&mut self) {
if !self.committed {
let _ = fs::remove_file(&self.temp_path);
}
}
}