use alloc::format;
use std::fs::File;
use std::path::{Path, PathBuf};
use crate::base::Error;
pub struct AtomicFile {
path: PathBuf,
destination: PathBuf,
persisted: bool,
}
impl AtomicFile {
pub fn create(destination: &Path) -> Result<(Self, File), Error> {
loop {
let path = Self::path_beside(destination)?;
match File::create_new(&path) {
Ok(file) => {
return Ok((
Self {
path,
destination: destination.to_path_buf(),
persisted: false,
},
file,
));
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => {
return Err(Error::IoError(format!(
"cannot create '{}': {e}",
path.display()
)));
}
}
}
}
pub fn path(&self) -> &Path {
&self.path
}
fn path_beside(destination: &Path) -> Result<PathBuf, Error> {
use core::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
let name = destination.file_name().ok_or_else(|| {
Error::IoError(format!(
"cannot write to '{}': not a file path",
destination.display()
))
})?;
let mut scratch = name.to_os_string();
scratch.push(format!(
".{}-{}.tmp",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
Ok(destination.with_file_name(scratch))
}
pub fn commit(mut self) -> Result<(), Error> {
File::options()
.write(true)
.open(&self.path)
.and_then(|file| file.sync_all())
.map_err(|e| {
Error::IoError(format!(
"cannot flush '{}' to disk: {e}",
self.path.display()
))
})?;
self.rename_onto()
}
#[cfg(unix)]
fn inherit_permissions(&self) -> Result<(), Error> {
let metadata = match std::fs::symlink_metadata(&self.destination) {
Ok(metadata) if metadata.is_file() => metadata,
_ => return Ok(()),
};
std::fs::set_permissions(&self.path, metadata.permissions()).map_err(|e| {
Error::IoError(format!(
"cannot carry the permissions of '{}' onto the file replacing it: {e}",
self.destination.display()
))
})
}
pub(crate) fn rename_onto(&mut self) -> Result<(), Error> {
#[cfg(unix)]
self.inherit_permissions()?;
std::fs::rename(&self.path, &self.destination).map_err(|e| {
Error::IoError(format!(
"cannot move the completed container '{}' onto '{}': {e}",
self.path.display(),
self.destination.display()
))
})?;
self.persisted = true;
#[cfg(unix)]
{
let parent = match self.destination.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => Path::new("."),
};
File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|e| {
Error::IoError(format!(
"'{}' was saved, but syncing its directory failed, so the rename may \
not survive power loss: {e}",
self.destination.display()
))
})?;
}
Ok(())
}
}
impl Drop for AtomicFile {
fn drop(&mut self) {
if !self.persisted {
let _ = std::fs::remove_file(&self.path);
}
}
}