├── .gitignore ├── appveyor.yml ├── Makefile ├── Cargo.toml ├── LICENSE ├── README.md ├── .github └── workflows │ └── msrv.yml ├── tests └── lib.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | atomicwrites-test.* 4 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | install: 3 | - ps: Start-FileDownload 'https://static.rust-lang.org/dist/rust-nightly-i686-pc-windows-gnu.exe' 4 | - rust-nightly-i686-pc-windows-gnu.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" 5 | - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin 6 | - rustc -V 7 | - cargo -V 8 | test_script: 9 | - cargo test --verbose 10 | 11 | artifacts: 12 | - path: ./target/ 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) 2 | THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd) 3 | 4 | test: 5 | cargo test 6 | 7 | build: 8 | cargo build 9 | 10 | docs: 11 | cd "$(THIS_DIR)" 12 | cp src/lib.rs code.bak 13 | cat README.md | sed -e 's/^/\/\/! /g' > readme.bak 14 | sed -i '/\/\/ INSERT_README_VIA_MAKE/r readme.bak' src/lib.rs 15 | (cargo doc --no-deps && make clean) || (make clean && false) 16 | 17 | clean: 18 | cd "$(THIS_DIR)" 19 | mv code.bak src/lib.rs || true 20 | rm *.bak || true 21 | 22 | upload: 23 | cd "$(THIS_DIR)" 24 | echo 'rust-atomicwrites' \ 25 | > ./target/doc/index.htm 26 | rsync -av --chmod=755 ./target/doc/ untispace:~/virtual/rust-atomicwrites.unterwaditzer.net/ 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atomicwrites" 3 | version = "0.4.4" 4 | edition = "2015" 5 | 6 | authors = ["Markus Unterwaditzer "] 7 | license = "MIT" 8 | keywords = ["filesystem", "posix"] 9 | categories = ["filesystem"] 10 | readme = "README.md" 11 | 12 | description = "Atomic file-writes." 13 | documentation = "https://docs.rs/crate/atomicwrites" 14 | homepage = "https://github.com/untitaker/rust-atomicwrites" 15 | repository = "https://github.com/untitaker/rust-atomicwrites" 16 | 17 | exclude = ["/.travis.yml", "/Makefile", "/appveyor.yml"] 18 | 19 | [dependencies] 20 | tempfile = "3.1" 21 | 22 | [target.'cfg(unix)'.dependencies] 23 | rustix = { version = "0.38.0", features = ["fs"] } 24 | 25 | [target.'cfg(windows)'.dependencies.windows-sys] 26 | version = "0.52.0" 27 | features = [ 28 | "Win32_Foundation", 29 | "Win32_Storage_FileSystem", 30 | ] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Markus Unterwaditzer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-atomicwrites 2 | 3 | [![Build Status](https://travis-ci.org/untitaker/rust-atomicwrites.svg?branch=master)](https://travis-ci.org/untitaker/rust-atomicwrites) 4 | [![Windows build status](https://ci.appveyor.com/api/projects/status/h6642x2d54xl0sev?svg=true)](https://ci.appveyor.com/project/untitaker/rust-atomicwrites) 5 | 6 | - [Documentation](https://docs.rs/crate/atomicwrites) 7 | - [Repository](https://github.com/untitaker/rust-atomicwrites) 8 | - [Crates.io](https://crates.io/crates/atomicwrites) 9 | 10 | Atomic file-writes. Works on both POSIX and Windows. 11 | 12 | The basic idea is to write to temporary files (in the same file 13 | system), and move them when done writing. 14 | This avoids the problem of two programs writing to the same file. For 15 | `AllowOverwrite`, `rename` is used. For `DisallowOverwrite`, `link + unlink` is 16 | used instead to raise errors when the target path already exists. 17 | 18 | This is mostly a port of the same-named [Python package](https://github.com/untitaker/python-atomicwrites). 19 | 20 | ## Example 21 | 22 | ```rust 23 | use atomicwrites::{AtomicFile,DisallowOverwrite}; 24 | 25 | let af = AtomicFile::new("foo", DisallowOverwrite); 26 | af.write(|f| { 27 | f.write_all(b"HELLO") 28 | })?; 29 | ``` 30 | 31 | ## Alternatives 32 | 33 | - [tempfile](https://github.com/Stebalien/tempfile) has a `persist` method doing the same thing. 34 | 35 | ## License 36 | 37 | Licensed under MIT, see ``LICENSE``. 38 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: MSRV 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust: 12 | - 1.63.0 13 | - stable 14 | - beta 15 | - nightly 16 | 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v2 20 | 21 | - name: Install toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: ${{ matrix.rust }} 25 | override: true 26 | 27 | - name: Run cargo check 28 | if: matrix.rust != 'nightly' 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: check 32 | 33 | - name: Run cargo check (nightly) 34 | if: matrix.rust == 'nightly' 35 | continue-on-error: true 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: check 39 | 40 | test: 41 | needs: [check] 42 | name: Test Suite 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | rust: 47 | - 1.63.0 48 | - stable 49 | - beta 50 | - nightly 51 | steps: 52 | - name: Checkout sources 53 | uses: actions/checkout@v2 54 | 55 | - name: Install toolchain 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | toolchain: ${{ matrix.rust }} 59 | override: true 60 | 61 | - name: Run cargo test 62 | if: matrix.rust != 'nightly' 63 | uses: actions-rs/cargo@v1 64 | with: 65 | command: test 66 | 67 | - name: Run cargo test (nightly) 68 | if: matrix.rust == 'nightly' 69 | continue-on-error: true 70 | uses: actions-rs/cargo@v1 71 | with: 72 | command: test 73 | 74 | fmt: 75 | needs: [check] 76 | name: Rustfmt 77 | runs-on: ubuntu-latest 78 | strategy: 79 | matrix: 80 | rust: 81 | - stable 82 | - beta 83 | steps: 84 | - name: Checkout sources 85 | uses: actions/checkout@v2 86 | 87 | - name: Install toolchain 88 | uses: actions-rs/toolchain@v1 89 | with: 90 | toolchain: ${{ matrix.rust }} 91 | override: true 92 | 93 | - name: Install rustfmt 94 | run: rustup component add rustfmt 95 | 96 | - name: Run cargo fmt 97 | uses: actions-rs/cargo@v1 98 | with: 99 | command: fmt 100 | args: --all -- --check 101 | 102 | clippy: 103 | needs: [check] 104 | name: Clippy 105 | runs-on: ubuntu-latest 106 | strategy: 107 | matrix: 108 | rust: 109 | - stable 110 | - beta 111 | - nightly 112 | steps: 113 | - name: Checkout sources 114 | uses: actions/checkout@v2 115 | 116 | - name: Install toolchain 117 | uses: actions-rs/toolchain@v1 118 | with: 119 | toolchain: ${{ matrix.rust }} 120 | override: true 121 | 122 | - name: Install clippy 123 | run: rustup component add clippy 124 | 125 | - name: Run cargo clippy 126 | uses: actions-rs/cargo@v1 127 | with: 128 | command: clippy 129 | args: -- -D warnings 130 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate atomicwrites; 2 | extern crate tempfile; 3 | 4 | use atomicwrites::{AllowOverwrite, AtomicFile, DisallowOverwrite}; 5 | use std::io::{self, Read, Write}; 6 | use std::{env, fs, path}; 7 | use tempfile::TempDir; 8 | 9 | fn get_tmp() -> path::PathBuf { 10 | TempDir::new().unwrap().into_path() 11 | } 12 | 13 | #[test] 14 | fn test_simple_allow_override() { 15 | let tmpdir = get_tmp(); 16 | let path = tmpdir.join("haha"); 17 | 18 | let af = AtomicFile::new(&path, AllowOverwrite); 19 | let res: io::Result<()> = af.write(|f| f.write_all(b"HELLO")).map_err(|x| x.into()); 20 | res.unwrap(); 21 | af.write(|f| f.write_all(b"HELLO")).unwrap(); 22 | 23 | let mut rv = String::new(); 24 | let mut testfd = fs::File::open(&path).unwrap(); 25 | testfd.read_to_string(&mut rv).unwrap(); 26 | assert_eq!(&rv[..], "HELLO"); 27 | } 28 | 29 | #[test] 30 | fn test_simple_disallow_override() { 31 | let tmpdir = get_tmp(); 32 | let path = tmpdir.join("haha"); 33 | 34 | let af = AtomicFile::new(&path, DisallowOverwrite); 35 | af.write(|f| f.write_all(b"HELLO")).unwrap(); 36 | assert!(af.write(|f| f.write_all(b"HELLO")).is_err()); 37 | 38 | let mut rv = String::new(); 39 | let mut testfd = fs::File::open(&path).unwrap(); 40 | testfd.read_to_string(&mut rv).unwrap(); 41 | assert_eq!(&rv[..], "HELLO"); 42 | } 43 | 44 | #[test] 45 | fn test_allowed_pathtypes() { 46 | AtomicFile::new("haha", DisallowOverwrite); 47 | AtomicFile::new(&"haha", DisallowOverwrite); 48 | AtomicFile::new(&path::Path::new("haha"), DisallowOverwrite); 49 | AtomicFile::new(&path::PathBuf::from("haha"), DisallowOverwrite); 50 | } 51 | 52 | #[test] 53 | fn test_unicode() { 54 | let dmitri = "Дмитрий"; 55 | let greeting = format!("HELLO {}", dmitri); 56 | 57 | let tmpdir = get_tmp(); 58 | let path = tmpdir.join(dmitri); 59 | 60 | let af = AtomicFile::new(&path, DisallowOverwrite); 61 | af.write(|f| f.write_all(greeting.as_bytes())).unwrap(); 62 | 63 | let mut rv = String::new(); 64 | let mut testfd = fs::File::open(&path).unwrap(); 65 | testfd.read_to_string(&mut rv).unwrap(); 66 | assert_eq!(rv, greeting); 67 | } 68 | 69 | #[test] 70 | fn test_weird_paths() { 71 | let tmpdir = get_tmp(); 72 | env::set_current_dir(tmpdir).expect("setup failed"); 73 | 74 | AtomicFile::new("foo", AllowOverwrite) 75 | .write(|f| f.write_all(b"HELLO")) 76 | .unwrap(); 77 | let mut rv = String::new(); 78 | let mut testfd = fs::File::open("foo").unwrap(); 79 | testfd.read_to_string(&mut rv).unwrap(); 80 | assert_eq!(rv, "HELLO"); 81 | } 82 | 83 | /// Test the error that is returned if the file already exists 84 | /// with `OverwriteBehavior::DisallowOverwrite`. 85 | #[test] 86 | fn disallow_overwrite_error() -> io::Result<()> { 87 | let tmp = TempDir::new()?; 88 | let file = tmp.path().join("dest"); 89 | let af = AtomicFile::new_with_tmpdir(&file, DisallowOverwrite, tmp.path()); 90 | 91 | // touch file 92 | fs::write(&file, "")?; 93 | 94 | match af.write(|f: &mut fs::File| f.write(b"abc")) { 95 | Ok(_) => panic!("should fail!"), 96 | Err(e) => { 97 | let e = io::Error::from(e); 98 | match e.kind() { 99 | io::ErrorKind::AlreadyExists => Ok(()), 100 | _ => Err(e), 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // INSERT_README_VIA_MAKE 2 | #[cfg(unix)] 3 | extern crate rustix; 4 | extern crate tempfile; 5 | 6 | use std::convert::AsRef; 7 | use std::error::Error as ErrorTrait; 8 | use std::fmt; 9 | use std::fs; 10 | use std::io; 11 | use std::path; 12 | 13 | pub use crate::OverwriteBehavior::{AllowOverwrite, DisallowOverwrite}; 14 | 15 | /// Whether to allow overwriting if the target file exists. 16 | #[derive(Clone, Copy)] 17 | pub enum OverwriteBehavior { 18 | /// Overwrite files silently. 19 | AllowOverwrite, 20 | 21 | /// Don't overwrite files. `AtomicFile.write` will raise errors for such conditions only after 22 | /// you've already written your data. 23 | DisallowOverwrite, 24 | } 25 | 26 | /// Represents an error raised by `AtomicFile.write`. 27 | #[derive(Debug)] 28 | pub enum Error { 29 | /// The error originated in the library itself, while it was either creating a temporary file 30 | /// or moving the file into place. 31 | Internal(io::Error), 32 | /// The error originated in the user-supplied callback. 33 | User(E), 34 | } 35 | 36 | /// If your callback returns a `std::io::Error`, you can unwrap this type to `std::io::Error`. 37 | impl From> for io::Error { 38 | fn from(e: Error) -> Self { 39 | match e { 40 | Error::Internal(x) => x, 41 | Error::User(x) => x, 42 | } 43 | } 44 | } 45 | 46 | impl fmt::Display for Error { 47 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 48 | match *self { 49 | Error::Internal(ref e) => e.fmt(f), 50 | Error::User(ref e) => e.fmt(f), 51 | } 52 | } 53 | } 54 | 55 | impl ErrorTrait for Error { 56 | fn cause(&self) -> Option<&dyn ErrorTrait> { 57 | match *self { 58 | Error::Internal(ref e) => Some(e), 59 | Error::User(ref e) => Some(e), 60 | } 61 | } 62 | } 63 | 64 | fn safe_parent(p: &path::Path) -> Option<&path::Path> { 65 | match p.parent() { 66 | None => None, 67 | Some(x) if x.as_os_str().is_empty() => Some(path::Path::new(".")), 68 | x => x, 69 | } 70 | } 71 | 72 | /// Create a file and write to it atomically, in a callback. 73 | pub struct AtomicFile { 74 | /// Path to the final file that is atomically written. 75 | path: path::PathBuf, 76 | overwrite: OverwriteBehavior, 77 | /// Directory to which to write the temporary subdirectories. 78 | tmpdir: path::PathBuf, 79 | } 80 | 81 | impl AtomicFile { 82 | /// Helper for writing to the file at `path` atomically, in write-only mode. 83 | /// 84 | /// If `OverwriteBehaviour::DisallowOverwrite` is given, 85 | /// an `Error::Internal` containing an `std::io::ErrorKind::AlreadyExists` 86 | /// will be returned from `self.write(...)` if the file exists. 87 | /// 88 | /// The temporary file is written to a temporary subdirectory in `.`, to ensure 89 | /// it’s on the same filesystem (so that the move is atomic). 90 | pub fn new

(path: P, overwrite: OverwriteBehavior) -> Self 91 | where 92 | P: AsRef, 93 | { 94 | let p = path.as_ref(); 95 | AtomicFile::new_with_tmpdir( 96 | p, 97 | overwrite, 98 | safe_parent(p).unwrap_or_else(|| path::Path::new(".")), 99 | ) 100 | } 101 | 102 | /// Like `AtomicFile::new`, but the temporary file is written to a temporary subdirectory in `tmpdir`. 103 | /// 104 | /// TODO: does `tmpdir` have to exist? 105 | pub fn new_with_tmpdir(path: P, overwrite: OverwriteBehavior, tmpdir: Q) -> Self 106 | where 107 | P: AsRef, 108 | Q: AsRef, 109 | { 110 | AtomicFile { 111 | path: path.as_ref().to_path_buf(), 112 | overwrite, 113 | tmpdir: tmpdir.as_ref().to_path_buf(), 114 | } 115 | } 116 | 117 | /// Move the file to `self.path()`. Only call once! Not exposed! 118 | fn commit(&self, tmppath: &path::Path) -> io::Result<()> { 119 | match self.overwrite { 120 | AllowOverwrite => replace_atomic(tmppath, self.path()), 121 | DisallowOverwrite => move_atomic(tmppath, self.path()), 122 | } 123 | } 124 | 125 | /// Get the target filepath. 126 | pub fn path(&self) -> &path::Path { 127 | &self.path 128 | } 129 | 130 | /// Open a temporary file, call `f` on it (which is supposed to write to it), then move the 131 | /// file atomically to `self.path`. 132 | /// 133 | /// The temporary file is written to a randomized temporary subdirectory with prefix `.atomicwrite`. 134 | pub fn write(&self, f: F) -> Result> 135 | where 136 | F: FnOnce(&mut fs::File) -> Result, 137 | { 138 | let mut options = fs::OpenOptions::new(); 139 | // These are the same options as `File::create`. 140 | options.write(true).create(true).truncate(true); 141 | self.write_with_options(f, options) 142 | } 143 | 144 | /// Open a temporary file with custom [`OpenOptions`], call `f` on it (which is supposed to 145 | /// write to it), then move the file atomically to `self.path`. 146 | /// 147 | /// The temporary file is written to a randomized temporary subdirectory with prefix 148 | /// `.atomicwrite`. 149 | /// 150 | /// [`OpenOptions`]: fs::OpenOptions 151 | pub fn write_with_options(&self, f: F, options: fs::OpenOptions) -> Result> 152 | where 153 | F: FnOnce(&mut fs::File) -> Result, 154 | { 155 | let tmpdir = tempfile::Builder::new() 156 | .prefix(".atomicwrite") 157 | .tempdir_in(&self.tmpdir) 158 | .map_err(Error::Internal)?; 159 | 160 | let tmppath = tmpdir.path().join("tmpfile.tmp"); 161 | let rv = { 162 | let mut tmpfile = options.open(&tmppath).map_err(Error::Internal)?; 163 | let r = f(&mut tmpfile).map_err(Error::User)?; 164 | tmpfile.sync_all().map_err(Error::Internal)?; 165 | r 166 | }; 167 | self.commit(&tmppath).map_err(Error::Internal)?; 168 | Ok(rv) 169 | } 170 | } 171 | 172 | #[cfg(unix)] 173 | mod imp { 174 | use super::safe_parent; 175 | 176 | use rustix::fs::AtFlags; 177 | use std::{fs, io, path}; 178 | 179 | pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 180 | let src_parent_path = safe_parent(src).unwrap(); 181 | let dst_parent_path = safe_parent(dst).unwrap(); 182 | let src_child_path = src.file_name().unwrap(); 183 | let dst_child_path = dst.file_name().unwrap(); 184 | 185 | // Open the parent directories. If src and dst have the same parent 186 | // path, open it once and reuse it. 187 | let src_parent = fs::File::open(src_parent_path)?; 188 | let dst_parent; 189 | let dst_parent = if src_parent_path == dst_parent_path { 190 | &src_parent 191 | } else { 192 | dst_parent = fs::File::open(dst_parent_path)?; 193 | &dst_parent 194 | }; 195 | 196 | // Do the `renameat`. 197 | rustix::fs::renameat(&src_parent, src_child_path, dst_parent, dst_child_path)?; 198 | 199 | // Fsync the parent directory (or directories, if they're different). 200 | src_parent.sync_all()?; 201 | if src_parent_path != dst_parent_path { 202 | dst_parent.sync_all()?; 203 | } 204 | 205 | Ok(()) 206 | } 207 | 208 | pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 209 | let src_parent_path = safe_parent(src).unwrap(); 210 | let dst_parent_path = safe_parent(dst).unwrap(); 211 | let src_child_path = src.file_name().unwrap(); 212 | let dst_child_path = dst.file_name().unwrap(); 213 | 214 | // Open the parent directories. If src and dst have the same parent 215 | // path, open it once and reuse it. 216 | let src_parent = fs::File::open(src_parent_path)?; 217 | let dst_parent; 218 | let dst_parent = if src_parent_path == dst_parent_path { 219 | &src_parent 220 | } else { 221 | dst_parent = fs::File::open(dst_parent_path)?; 222 | &dst_parent 223 | }; 224 | 225 | // On Linux, use `renameat2` with `RENAME_NOREPLACE` if we have it, as 226 | // that does an atomic rename. 227 | #[cfg(any(target_os = "android", target_os = "linux"))] 228 | { 229 | use rustix::fs::RenameFlags; 230 | use std::sync::atomic::AtomicBool; 231 | use std::sync::atomic::Ordering::Relaxed; 232 | 233 | static NO_RENAMEAT2: AtomicBool = AtomicBool::new(false); 234 | if !NO_RENAMEAT2.load(Relaxed) { 235 | match rustix::fs::renameat_with( 236 | &src_parent, 237 | src_child_path, 238 | dst_parent, 239 | dst_child_path, 240 | RenameFlags::NOREPLACE, 241 | ) { 242 | Ok(()) => { 243 | // Fsync the parent directory (or directories, if 244 | // they're different). 245 | src_parent.sync_all()?; 246 | if src_parent_path != dst_parent_path { 247 | dst_parent.sync_all()?; 248 | } 249 | return Ok(()); 250 | } 251 | Err(rustix::io::Errno::INVAL) | Err(rustix::io::Errno::NOSYS) => { 252 | // `NOSYS` means the OS doesn't support `renameat2`; 253 | // remember this so that we don't bother calling it 254 | // again. 255 | // 256 | // `INVAL` might mean we're on a filesystem that 257 | // doesn't support the `NOREPLACE` flag, such as ZFS, 258 | // so let's conservatively avoid using `renameat2` 259 | // again as well. 260 | // 261 | // (Or, `INVAL` might mean that the user is trying to 262 | // make a directory a subdirectory of itself, in which 263 | // case responding by disabling further use of 264 | // `renameat2` is unfortunate but what else can we do?) 265 | NO_RENAMEAT2.store(true, Relaxed); 266 | } 267 | Err(e) => return Err(e.into()), 268 | } 269 | } 270 | } 271 | 272 | // Otherwise, hard-link the src to the dst, and then delete the dst. 273 | rustix::fs::linkat( 274 | &src_parent, 275 | src_child_path, 276 | dst_parent, 277 | dst_child_path, 278 | AtFlags::empty(), 279 | )?; 280 | rustix::fs::unlinkat(&src_parent, src_child_path, AtFlags::empty())?; 281 | 282 | // Fsync the parent directory (or directories, if they're different). 283 | src_parent.sync_all()?; 284 | if src_parent_path != dst_parent_path { 285 | dst_parent.sync_all()?; 286 | } 287 | 288 | Ok(()) 289 | } 290 | } 291 | 292 | #[cfg(windows)] 293 | mod imp { 294 | extern crate windows_sys; 295 | 296 | use std::ffi::OsStr; 297 | use std::os::windows::ffi::OsStrExt; 298 | use std::{io, path}; 299 | 300 | macro_rules! call { 301 | ($e: expr) => { 302 | if $e != 0 { 303 | Ok(()) 304 | } else { 305 | Err(io::Error::last_os_error()) 306 | } 307 | }; 308 | } 309 | 310 | fn path_to_windows_str>(x: T) -> Vec { 311 | x.as_ref().encode_wide().chain(Some(0)).collect() 312 | } 313 | 314 | pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 315 | call!(unsafe { 316 | windows_sys::Win32::Storage::FileSystem::MoveFileExW( 317 | path_to_windows_str(src).as_ptr(), 318 | path_to_windows_str(dst).as_ptr(), 319 | windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH 320 | | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING, 321 | ) 322 | }) 323 | } 324 | 325 | pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 326 | call!(unsafe { 327 | windows_sys::Win32::Storage::FileSystem::MoveFileExW( 328 | path_to_windows_str(src).as_ptr(), 329 | path_to_windows_str(dst).as_ptr(), 330 | windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH, 331 | ) 332 | }) 333 | } 334 | } 335 | 336 | /// Move `src` to `dst`. If `dst` exists, it will be silently overwritten. 337 | /// 338 | /// Both paths must reside on the same filesystem for the operation to be atomic. 339 | pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 340 | imp::replace_atomic(src, dst) 341 | } 342 | 343 | /// Move `src` to `dst`. An error will be returned if `dst` exists. 344 | /// 345 | /// Both paths must reside on the same filesystem for the operation to be atomic. 346 | pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> { 347 | imp::move_atomic(src, dst) 348 | } 349 | --------------------------------------------------------------------------------