save-data 0.1.0

save data safely
Documentation
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 {
        // `file_path` is "/" or ""
        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 {
                    // This is very rare case. We retry it.
                    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(())
        }
        // If `file_path` not exists yet, we can't make backup file.
        Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(e),
    }
}

/// The data read from `src` is saved to `file_path`.
///
/// The file is updated safely.
/// This means that even if the operation is interrupted,
/// `file_path` is guaranteed to contain either the complete old data or the complete new data.
/// However, it is assumed that the data is permanently written to storage when
/// [`File::fsync_all()`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all)
/// completes successfully.
///
/// If `file_path` exists, the data is first written to a temporary file,
/// then the file is replaced using `rename`.
/// Additionally, a backup file of the original file is created.
/// The path of the backup file is `file_path` appended with `.orig`.
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());
    }
}