use alloc::format;
use alloc::string::ToString;
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> {
self.sync_scratch()?;
self.rename_onto()
}
pub fn commit_new(mut self) -> Result<(), Error> {
self.sync_scratch()?;
self.link_onto()
}
fn sync_scratch(&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()
))
})
}
#[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;
self.sync_parent()
}
pub(crate) fn link_onto(&mut self) -> Result<(), Error> {
use std::io::ErrorKind;
match std::fs::hard_link(&self.path, &self.destination) {
Ok(()) => {}
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
return Err(Error::AlreadyExists(self.destination.display().to_string()));
}
Err(link_error) => {
return match std::fs::symlink_metadata(&self.destination) {
Ok(_) => Err(Error::AlreadyExists(self.destination.display().to_string())),
Err(e) if e.kind() == ErrorKind::NotFound => {
self.rename_onto().map_err(|e| match e {
Error::IoError(message) => Error::IoError(format!(
"{message} (after linking failed: {link_error})"
)),
other => other,
})
}
Err(e) => Err(Error::IoError(format!(
"cannot link '{}' onto '{}' ({link_error}), nor check whether it \
exists: {e}",
self.path.display(),
self.destination.display()
))),
};
}
}
self.persisted = true;
std::fs::remove_file(&self.path).map_err(|e| {
Error::IoError(format!(
"'{}' was saved, but its scratch link '{}' could not be removed: {e}",
self.destination.display(),
self.path.display()
))
})?;
self.sync_parent()
}
fn sync_parent(&self) -> Result<(), Error> {
#[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 it 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);
}
}
}