diff options
author | Stefan Kreutz <mail@skreutz.com> | 2024-03-24 15:04:09 +0100 |
---|---|---|
committer | Stefan Kreutz <mail@skreutz.com> | 2024-03-24 15:04:09 +0100 |
commit | c1fa48e9bd617d70e823efef5d6dcea41b1d2087 (patch) | |
tree | 421e69c512ac54bf65495ef23fd7d9ec5a5e67d5 /src/fs.rs | |
download | brck-0.1.0.tar |
Add initial implementationbrck-0.1.0
Diffstat (limited to 'src/fs.rs')
-rw-r--r-- | src/fs.rs | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..f962e53 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,144 @@ +//! Filesytem functionality. +//! +//! This module contains several extensions to [std::fs]. + +use std::{ + fs::{File, OpenOptions}, + path::{Path, PathBuf}, +}; + +/// A std::fs::File wrapper that removes the file when dropped. +#[derive(Debug)] +pub struct TmpFile { + path: PathBuf, + file: File, +} + +impl TmpFile { + /// Opens a new temporary file at the given path. + pub fn open<P: AsRef<Path>>(path: P) -> Result<TmpFile, std::io::Error> { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let file = options.open(&path)?; + Ok(TmpFile { + path: path.as_ref().into(), + file, + }) + } + + pub fn file(&self) -> &File { + &self.file + } +} + +impl Drop for TmpFile { + fn drop(&mut self) { + if let Err(err) = remove_file_if_exists(&self.path) { + eprintln!( + "Failed to remove temporary file {}: {}", + &self.path.display(), + err + ); + } + } +} + +/// Removes a file from the filesystem, if it exists. +pub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> { + std::fs::remove_file(path).or_else(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + Ok(()) + } else { + Err(err) + } + }) +} + +/// Returns the path without it's final component, if there is one. +/// +/// In contrast to [Path::parent], this function returns the [CurDir][std::path::Component::CurDir] +/// instead of the empty string for the current directory. +pub fn parent_dir<P: AsRef<Path>>(path: &P) -> Option<&Path> { + path.as_ref().parent().map(|p| { + if p.as_os_str().is_empty() { + Path::new(std::path::Component::CurDir.as_os_str()) + } else { + p + } + }) +} + +/// Rename a file. +/// +/// This function should be atomic on POSIX-conform systems. +/// +/// Both paths must be located on the same filesystem. +pub fn rename_file<P, Q>(from: P, to: Q) -> Result<(), std::io::Error> +where + P: AsRef<Path>, + Q: AsRef<Path>, +{ + File::open(&from)?.sync_all()?; + std::fs::rename(&from, &to)?; + + let dest_dir = parent_dir(&to).ok_or_else(|| std::io::Error::other("is root directory"))?; + File::open(dest_dir)?.sync_all()?; + + let src_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?; + if src_dir != dest_dir { + File::open(src_dir)?.sync_all()?; + } + + Ok(()) +} + +/// Copy a file. +/// +/// This functions hard links `from` to `tmp`, and then renames the latter to `to`. The renaming +/// should be atomic on POSIX-conform systems. +/// +/// All three paths must be located on the same filesystem. +// Didn't use https://crates.io/crates/atomic-write-file or https://crates.io/crates/atomicwrites +// because they create randomly named temporary files, which I wanted to unveil(2) on OpenBSD, and +// remove when receiving a termination signal. +pub fn copy_file<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error> +where + P: AsRef<Path>, + Q: AsRef<Path>, + R: AsRef<Path>, +{ + fn inner<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error> + where + P: AsRef<Path>, + Q: AsRef<Path>, + R: AsRef<Path>, + { + let tmp_dir = + parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?; + File::open(tmp_dir)?.sync_all()?; + + std::fs::rename(&tmp, &to)?; + + let to_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?; + File::open(to_dir)?.sync_all()?; + + Ok(()) + } + + File::open(&from)?.sync_all()?; + std::fs::hard_link(&from, &tmp)?; + inner(&from, &tmp, &to).inspect_err(|_| { + if let Err(err) = remove_file_if_exists(&tmp) { + eprintln!( + "Failed to remove temporary file {}: {}", + tmp.as_ref().display(), + err + ); + } + }) +} |