summaryrefslogtreecommitdiff
path: root/src/fs.rs
diff options
context:
space:
mode:
authorStefan Kreutz <mail@skreutz.com>2024-03-24 15:04:09 +0100
committerStefan Kreutz <mail@skreutz.com>2024-03-24 15:04:09 +0100
commitc1fa48e9bd617d70e823efef5d6dcea41b1d2087 (patch)
tree421e69c512ac54bf65495ef23fd7d9ec5a5e67d5 /src/fs.rs
downloadbrck-c1fa48e9bd617d70e823efef5d6dcea41b1d2087.tar
Add initial implementationbrck-0.1.0
Diffstat (limited to 'src/fs.rs')
-rw-r--r--src/fs.rs144
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
+ );
+ }
+ })
+}
Generated by cgit. See skreutz.com for my tech blog and contact information.