use std::{
ffi::{OsStr, OsString},
fs::{self, File},
io,
path::{Path, PathBuf},
};
fn is_invalid_path(file_path: &Path) -> io::Result<bool> {
match std::fs::symlink_metadata(file_path) {
Ok(metadata) => return Ok(!metadata.is_file()),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
if file_path.file_name().is_none() {
return Ok(true);
}
let Some(parent) = file_path.parent() else {
return Ok(true);
};
Ok(!(parent == Path::new("") || parent.is_dir()))
}
fn parent_dir(file_path: &Path) -> &Path {
let mut parent_dir = file_path.parent().unwrap();
if parent_dir == Path::new("") {
parent_dir = Path::new(".");
}
parent_dir
}
fn open_dir(path: &Path) -> io::Result<File> {
Ok(File::from(nix::fcntl::open(
path,
nix::fcntl::OFlag::O_RDONLY | nix::fcntl::OFlag::O_DIRECTORY,
nix::sys::stat::Mode::empty(),
)?))
}
fn create_temp_file(parent_path: &Path) -> io::Result<(File, PathBuf)> {
let template = parent_path.join("tmp.XXXXXX");
let (fd, path) = nix::unistd::mkstemp(&template)?;
Ok((File::from(fd), path))
}
fn write_to_temp_file_with_sync<R: io::Read>(
parent_path: &Path,
mut src: R,
) -> io::Result<PathBuf> {
let (mut temp_file, temp_file_path) = create_temp_file(parent_path)?;
io::copy(&mut src, &mut temp_file)?;
temp_file.sync_all()?;
Ok(temp_file_path)
}
fn add_extension<S: AsRef<OsStr>>(path: &Path, ext: S) -> PathBuf {
let mut file_name = path.file_name().unwrap().to_owned();
file_name.push(".");
file_name.push(ext.as_ref());
path.with_file_name(file_name)
}
fn generate_random_temp_file_name(len: usize) -> OsString {
use rand::distr::{Alphanumeric, SampleString};
let mut temp_file_name = OsString::from(Alphanumeric.sample_string(&mut rand::rng(), len));
temp_file_name.push(".tmp");
temp_file_name
}
fn generate_temp_file_path(parent: Option<&Path>) -> PathBuf {
let temp_file_name = generate_random_temp_file_name(10);
if let Some(parent) = parent {
parent.join(temp_file_name)
} else {
PathBuf::from(temp_file_name)
}
}
fn retry_loop<T, F>(file_path: &Path, func: F) -> io::Result<(T, PathBuf)>
where
F: Fn(&Path) -> io::Result<T>,
{
let parent = file_path.parent();
let mut retry_remain = 3;
loop {
let temp_file_path = generate_temp_file_path(parent);
match func(&temp_file_path) {
Ok(value) => return Ok((value, temp_file_path)),
Err(e) => {
if e.kind() == io::ErrorKind::AlreadyExists && retry_remain > 0 {
retry_remain -= 1;
} else {
return Err(e);
}
}
}
}
}
fn hard_link_to_temp_file(file_path: &Path) -> io::Result<PathBuf> {
Ok(retry_loop(file_path, |path| fs::hard_link(file_path, path))?.1)
}
fn make_backup_file(file_path: &Path) -> io::Result<()> {
let backup_path = add_extension(file_path, "orig");
match hard_link_to_temp_file(file_path) {
Ok(temp_file_path) => {
fs::rename(temp_file_path, backup_path)?;
Ok(())
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
pub fn save_data<P: AsRef<Path>, R: io::Read>(file_path: P, src: R) -> io::Result<()> {
let file_path = file_path.as_ref();
if is_invalid_path(file_path)? {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let parent_path = parent_dir(file_path);
let parent_dir = open_dir(parent_path)?;
let temp_file_path = write_to_temp_file_with_sync(parent_path, src)?;
parent_dir.sync_all()?;
make_backup_file(file_path)?;
parent_dir.sync_all()?;
fs::rename(temp_file_path, file_path)?;
parent_dir.sync_all()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn write_read(path: &Path, data: &[u8]) {
save_data(path, data).unwrap();
let data2 = std::fs::read("foo.bin").unwrap();
assert_eq!(data, &data2);
}
#[test]
fn it_works() {
let mut data = [0u8; 4096];
for _ in 0..10 {
rand::fill(&mut data);
write_read("foo.bin".as_ref(), &data);
}
}
#[test]
fn path_test() {
assert!(is_invalid_path(Path::new("/a/b/..")).unwrap());
assert!(is_invalid_path(Path::new("/")).unwrap());
assert!(is_invalid_path(Path::new("")).unwrap());
}
}