From de44c940677e674a95f1431677e29f15d7dfa9db Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Fri, 3 Mar 2023 21:24:41 -0800 Subject: [PATCH 1/7] misc: use rustdoc instead of doc --- justfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/justfile b/justfile index 58aa4af..b8db1f4 100644 --- a/justfile +++ b/justfile @@ -67,9 +67,8 @@ doc_dir := justfile_directory() + "/doc" # env RUSTUP_PERMIT_COPY_RENAME=true rustup install nightly # Make the docs. - cargo doc \ + cargo rustdoc \ --release \ - --no-deps \ --target x86_64-unknown-linux-gnu \ --target-dir "{{ cargo_dir }}" From e9154f54a0614c228440a8661a7b5df076d1a568 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Fri, 3 Mar 2023 21:25:49 -0800 Subject: [PATCH 2/7] cleanup: remove redundant linux specialization (`tempfile` handles it now) --- CHANGELOG.md | 10 +++ Cargo.toml | 6 +- README.md | 10 +-- src/fallback.rs | 24 ------- src/lib.rs | 173 ++++++++++++------------------------------------ src/linux.rs | 73 -------------------- 6 files changed, 59 insertions(+), 237 deletions(-) delete mode 100644 src/fallback.rs delete mode 100644 src/linux.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f9d4e..9b9604f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ +## [0.3.0](https://github.com/Blobfolio/write_atomic/releases/tag/v0.3.0) - TBD + +### Changed + +* Use `tempfile` for all temporary file writes (it now natively supports `O_TMPFILE`); +* Replace `libc::fchown` with `rustix::fs::fchown` for better parity with `tempfile`'s dependencies; +* Improve performance of `copy_file`; + + + ## [0.2.10](https://github.com/Blobfolio/write_atomic/releases/tag/v0.2.10) - 2023-03-03 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 6cbbffe..a439349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ man-dir = "./" credits-dir = "./" [dependencies] -fastrand = ">=1.7.0, <=1.9.0" tempfile = "=3.4.0" -[target.'cfg(unix)'.dependencies] -libc = ">= 0.2.34" +[target.'cfg(unix)'.dependencies.rustix] +version = "0.36.0" # Match the version required by tempfile. +features = [ "fs", "process" ] diff --git a/README.md b/README.md index 2e012f4..e3f77de 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,11 @@ [![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL) [![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/write_atomic/issues) -**ALPHA**: Note this crate is a work-in-progress and is not yet ready for production use. +Write Atomic was originally a stripped-down remake of [`tempfile-fast`](https://crates.io/crates/tempfile-fast), but with the `3.4.0` release of [`tempfile`](https://crates.io/crates/tempfile), it has largely been mooted. -Write Atomic is a stripped-down remake of [`tempfile-fast`](https://crates.io/crates/tempfile-fast), boiling everything down to a single method: [`write_file`]. +(`tempfile` now supports Linux optimizations like `O_TMPFILE` natively.) -Like `tempfile-fast`, bytes will first be written to a temporary file — either `O_TMPFILE` on supporting Linux systems or via the [`tempfile`](https://crates.io/crates/tempfile) crate — then moved the final destination. - -When overwriting an existing file, permissions and ownership will be preserved, otherwise the permissions and ownership will default to the same values you'd get if using `std::fs::File::create`. - -Because there is just a single [`write_file`] method, this crate is only really suitable in cases where you have the path and all the bytes you want to write ready to go. If you need more granular `Read`/`Seek`/`Write` support, use `tempfile-fast` instead. +That said, one might still enjoy the ergonomic single-shot nature of Write Atomic's `write_file` and `copy_file` methods, as well as their permission/ownership-syncing behaviors, and so it lives on! diff --git a/src/fallback.rs b/src/fallback.rs deleted file mode 100644 index 3bb5172..0000000 --- a/src/fallback.rs +++ /dev/null @@ -1,24 +0,0 @@ -/*! -# Write Atomic - Fallback. -*/ - -use std::{ - fs::File, - io::{ - Result, - ErrorKind, - }, - path::Path, -}; - -#[inline] -pub(super) fn nonexclusive_tempfile

(_dir: P) -> Result -where P: AsRef { - Err(ErrorKind::InvalidInput.into()) -} - -#[inline] -pub(super) fn link_at

(_what: &File, _dst: P) -> Result<()> -where P: AsRef { - Err(ErrorKind::InvalidData.into()) -} diff --git a/src/lib.rs b/src/lib.rs index 96bb8b7..6c5456e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,15 +9,11 @@ [![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL) [![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/write_atomic/issues) -**ALPHA**: Note this crate is a work-in-progress and is not yet ready for production use. +Write Atomic was originally a stripped-down remake of [`tempfile-fast`](https://crates.io/crates/tempfile-fast), but with the `3.4.0` release of [`tempfile`](https://crates.io/crates/tempfile), it has largely been mooted. -Write Atomic is a stripped-down remake of [`tempfile-fast`](https://crates.io/crates/tempfile-fast), boiling everything down to a single method: [`write_file`]. +(`tempfile` now supports Linux optimizations like `O_TMPFILE` natively.) -Like `tempfile-fast`, bytes will first be written to a temporary file — either `O_TMPFILE` on supporting Linux systems or via the [`tempfile`](https://crates.io/crates/tempfile) crate — then moved the final destination. - -When overwriting an existing file, permissions and ownership will be preserved, otherwise the permissions and ownership will default to the same values you'd get if using [`std::fs::File::create`]. - -Because there is just a single [`write_file`] method, this crate is only really suitable in cases where you have the path and all the bytes you want to write ready to go. If you need more granular `Read`/`Seek`/`Write` support, use `tempfile-fast` instead. +That said, one might still enjoy the ergonomic single-shot nature of Write Atomic's [`write_file`] and [`copy_file`] methods, as well as their permission/ownership-syncing behaviors, and so it lives on! ## Examples @@ -32,7 +28,7 @@ Add `write_atomic` to your `dependencies` in `Cargo.toml`, like: ```text,ignore [dependencies] -write_atomic = "0.2.*" +write_atomic = "0.3.*" ``` */ @@ -61,19 +57,8 @@ write_atomic = "0.2.*" unused_import_braces, )] -#![allow( - clippy::module_name_repetitions, - clippy::redundant_pub_crate, -)] - - -#[cfg(target_os = "linux")] mod linux; -#[cfg(not(target_os = "linux"))] mod fallback; - - -#[cfg(not(target_os = "linux"))] use fallback as linux; use std::{ fs::File, io::{ @@ -92,12 +77,13 @@ use tempfile::NamedTempFile; -/// # Atomic Copy File! +/// # Atomic File Copy! /// /// This will copy the contents of one file to another, atomically. /// -/// It is similar to [`std::fs::copy`], but uses atomic writes and syncs -/// ownership in addition to permissions (on Unix). +/// Under the hood, this uses [`std::fs::copy`] to copy the file to a temporary +/// location. It then syncs the file permissions — and on Unix, the owner/group +/// — before moving it to the final destination. /// /// See [`write_file`] for more details about atomicity. /// @@ -108,15 +94,18 @@ use tempfile::NamedTempFile; pub fn copy_file

(src: P, dst: P) -> Result<()> where P: AsRef { let src = src.as_ref(); - let dst = dst.as_ref(); - let raw = std::fs::read(src)?; - write_file(dst, &raw)?; + let (dst, parent) = check_path(dst)?; - if let Ok(file) = File::open(dst) { - let _res = copy_metadata(src, &file); - } + let file = tempfile::Builder::new().tempfile_in(parent)?; + std::fs::copy(src, &file)?; - Ok(()) + let touched = touch_if(&dst)?; + if let Err(e) = write_finish(file, &dst) { + // If we created the file earlier, try to remove it. + if touched { let _res = std::fs::remove_file(dst); } + Err(e) + } + else { Ok(()) } } /// # Atomic File Write! @@ -125,10 +114,6 @@ where P: AsRef { /// permissions and ownership if it already exists, or creating it anew using /// the same default permissions and ownership [`std::fs::File::create`] would. /// -/// Atomicity is achieved by first writing the content to a temporary location. -/// On most Linux systems, this will use `O_TMPFILE`; for other systems, the -/// [`tempfile`] crate will be used instead. -/// /// ## Examples /// /// ```no_run @@ -143,20 +128,20 @@ where P: AsRef { /// way. pub fn write_file

(src: P, data: &[u8]) -> Result<()> where P: AsRef { - let (src, parent) = check_path(src)?; + let (dst, parent) = check_path(src)?; - // Write via O_TMPFILE if we can. - if let Ok(file) = linux::nonexclusive_tempfile(&parent) { - write_direct(BufWriter::new(file), &src, data) - } - // Otherwise fall back to the trusty `tempfile`. - else { - write_fallback( - BufWriter::new(tempfile::Builder::new().tempfile_in(parent)?), - &src, - data, - ) + let mut file = BufWriter::new(tempfile::Builder::new().tempfile_in(parent)?); + file.write_all(data)?; + file.flush()?; + let file = file.into_inner()?; + + let touched = touch_if(&dst)?; + if let Err(e) = write_finish(file, &dst) { + // If we created the file earlier, try to remove it. + if touched { let _res = std::fs::remove_file(dst); } + Err(e) } + else { Ok(()) } } @@ -219,16 +204,16 @@ fn copy_metadata(src: &Path, dst: &File) -> Result<()> { #[allow(unsafe_code)] /// # Copy Ownership. /// -/// On Unix systems, we need to copy ownership in addition to permissions. -fn copy_ownership(source: &std::fs::Metadata, dst: &File) -> Result<()> { - use std::os::unix::{ - fs::MetadataExt, - io::AsRawFd, - }; - - let fd = dst.as_raw_fd(); - if 0 == unsafe { libc::fchown(fd, source.uid(), source.gid()) } { Ok(()) } - else { Err(Error::last_os_error()) } +/// Copy the owner/group details from `src` to `dst`. +fn copy_ownership(src: &std::fs::Metadata, dst: &File) -> Result<()> { + use rustix::process::{Gid, Uid}; + use std::os::unix::fs::MetadataExt; + + rustix::fs::fchown( + dst, + Some(unsafe { Uid::from_raw(src.uid()) }), + Some(unsafe { Gid::from_raw(src.gid()) }), + ).map_err(Into::into) } /// # Touch If Needed. @@ -243,82 +228,10 @@ fn touch_if(src: &Path) -> Result { } } -/// # Write Direct. -/// -/// This is an optimized file write for modern Linux installs. -fn write_direct(mut file: BufWriter, dst: &Path, data: &[u8]) -> Result<()> { - file.write_all(data)?; - file.flush()?; - let mut file = file.into_inner()?; - - let touched = touch_if(dst)?; - match write_direct_end(&mut file, dst) { - Ok(()) => Ok(()), - Err(e) => { - // If we created the file earlier, try to remove it. - if touched { let _res = std::fs::remove_file(dst); } - Err(e) - } - } -} - -/// # Finish Write Direct. -fn write_direct_end(file: &mut File, dst: &Path) -> Result<()> { - // Copy metadata. - copy_metadata(dst, file)?; - - // If linking works right off the bat, hurray! - if linux::link_at(file, dst).is_ok() { - return Ok(()); - } - - // Otherwise we need a a unique location. - let mut dst_tmp = dst.to_path_buf(); - for _ in 0..32768 { - // Build a new file name. - dst_tmp.pop(); - dst_tmp.push(format!(".{:x}.tmp", fastrand::u64(..))); - - match linux::link_at(file, &dst_tmp) { - Ok(()) => return std::fs::rename(&dst_tmp, dst).map_err(|e| { - // That didn't work; attempt cleanup. - let _res = std::fs::remove_file(&dst_tmp); - e - }), - Err(e) => { - // Collisions just require another go; for other errors, we - // should abort. - if ErrorKind::AlreadyExists != e.kind() { return Err(e); } - } - }; - } - - // If we're here, we've failed. - Err(Error::new(ErrorKind::Other, "Couldn't create a temporary file.")) -} - -/// # Write Fallback. +/// # Finish Write. /// -/// For systems where `O_TMPFILE` is unavailable, we can just use the -/// `tempfile` crate. -fn write_fallback(mut file: BufWriter, dst: &Path, data: &[u8]) -> Result<()> { - file.write_all(data)?; - file.flush()?; - let file = file.into_inner()?; - - let touched = touch_if(dst)?; - match write_fallback_finish(file, dst) { - Ok(()) => Ok(()), - Err(e) => { - // If we created the file earlier, try to remove it. - if touched { let _res = std::fs::remove_file(dst); } - Err(e) - } - } -} - -/// # Finish Write Fallback. -fn write_fallback_finish(file: NamedTempFile, dst: &Path) -> Result<()> { +/// This attempts to copy the metadata, then persist the tempfile. +fn write_finish(file: NamedTempFile, dst: &Path) -> Result<()> { copy_metadata(dst, file.as_file()) .and_then(|_| file.persist(dst).map(|_| ()).map_err(|e| e.error)) } diff --git a/src/linux.rs b/src/linux.rs deleted file mode 100644 index 568d5b0..0000000 --- a/src/linux.rs +++ /dev/null @@ -1,73 +0,0 @@ -/*! -# Write Atomic - Linux Optimizations - -This module has been adapted from [`tempfile-fast`](https://github.com/FauxFaux/tempfile-fast-rs/blob/7cd84b28029250c96970265141448a04cafb8f60/src/linux.rs). -*/ - -use libc::{ - AT_FDCWD, - AT_SYMLINK_FOLLOW, - O_CLOEXEC, - O_RDWR, - O_TMPFILE, -}; -use std::{ - ffi::CString, - fs::File, - io::{ - Error, - ErrorKind, - Result, - }, - os::unix::{ - ffi::OsStrExt, - io::{ - AsRawFd, - FromRawFd, - }, - }, - path::Path, -}; - - - -#[allow(unsafe_code)] -/// # Create Non-exclusive Tempfile. -pub(super) fn nonexclusive_tempfile

(dir: P) -> Result -where P: AsRef { - let path = cstr(dir)?; - match unsafe { libc::open64(path.as_ptr(), O_CLOEXEC | O_TMPFILE | O_RDWR, 0o600) } { - -1 => Err(ErrorKind::InvalidInput.into()), - fd => Ok(unsafe { FromRawFd::from_raw_fd(fd) }), - } -} - -#[allow(unsafe_code)] -/// # Link At. -/// -/// Attempt to update the file system link for a given file. -pub(super) fn link_at

(what: &File, dst: P) -> Result<()> -where P: AsRef { - let old_path: CString = CString::new(format!("/proc/self/fd/{}", what.as_raw_fd()))?; - let new_path = cstr(dst)?; - - unsafe { - if 0 == libc::linkat( - AT_FDCWD, - old_path.as_ptr().cast(), - AT_FDCWD, - new_path.as_ptr().cast(), - AT_SYMLINK_FOLLOW, - ) { Ok(()) } - else { Err(Error::last_os_error()) } - } -} - - - -/// # `Path` to `CString` -fn cstr

(path: P) -> Result -where P: AsRef { - CString::new(path.as_ref().as_os_str().as_bytes()) - .map_err(|_| Error::new(ErrorKind::InvalidInput, "Path contains a null.")) -} From cff7e145fa34b8501fb4ec395d69d7432e99d2dc Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 8 Mar 2023 22:34:02 -0800 Subject: [PATCH 3/7] ci: temporarily test all branches --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 04c184c..25705e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ master ] + branches: [ ] pull_request: - branches: [ master ] + branches: [ ] defaults: run: From 7e9b30430a7ebff340a2e1c93ab5d37f26022468 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 8 Mar 2023 22:36:03 -0800 Subject: [PATCH 4/7] docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3f77de..418cb7c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add `write_atomic` to your `dependencies` in `Cargo.toml`, like: ``` [dependencies] -write_atomic = "0.2.*" +write_atomic = "0.3.*" ``` From 0534d58edef91e3dd0455fffe253383504a665e7 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 8 Mar 2023 22:37:19 -0800 Subject: [PATCH 5/7] bump: 0.3.0 --- CREDITS.md | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 81c403a..71fe64b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,7 +1,7 @@ # Project Dependencies Package: write_atomic - Version: 0.2.10 - Generated: 2023-03-03 19:20:45 UTC + Version: 0.3.0 + Generated: 2023-03-09 06:36:28 UTC | Package | Version | Author(s) | License | | ---- | ---- | ---- | ---- | diff --git a/Cargo.toml b/Cargo.toml index a439349..257cbcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "write_atomic" -version = "0.2.10" +version = "0.3.0" authors = ["Blobfolio, LLC. "] edition = "2021" rust-version = "1.56" From 86a8c874e4692b6b6aec0e5ecb15e5d00304741a Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 8 Mar 2023 22:54:14 -0800 Subject: [PATCH 6/7] ci: restore master fitler --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25705e4..04c184c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ ] + branches: [ master ] pull_request: - branches: [ ] + branches: [ master ] defaults: run: From c70fc77de0e6d753ab1738eedf5e1f6122c976b5 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Thu, 9 Mar 2023 08:43:05 -0800 Subject: [PATCH 7/7] build: 0.3.0 --- CHANGELOG.md | 2 +- CREDITS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9604f..4a663e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ -## [0.3.0](https://github.com/Blobfolio/write_atomic/releases/tag/v0.3.0) - TBD +## [0.3.0](https://github.com/Blobfolio/write_atomic/releases/tag/v0.3.0) - 2023-03-09 ### Changed diff --git a/CREDITS.md b/CREDITS.md index 71fe64b..7902e56 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,7 +1,7 @@ # Project Dependencies Package: write_atomic Version: 0.3.0 - Generated: 2023-03-09 06:36:28 UTC + Generated: 2023-03-09 16:42:54 UTC | Package | Version | Author(s) | License | | ---- | ---- | ---- | ---- |