//! 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>(path: P) -> Result { 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>(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>(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(from: P, to: Q) -> Result<(), std::io::Error> where P: AsRef, Q: AsRef, { 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(from: P, tmp: Q, to: R) -> Result<(), std::io::Error> where P: AsRef, Q: AsRef, R: AsRef, { fn inner(from: P, tmp: Q, to: R) -> Result<(), std::io::Error> where P: AsRef, Q: AsRef, R: AsRef, { 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 ); } }) }