Line data Source code
1 : use nix::NixPath;
2 :
3 : /// Rename a file without replacing an existing file.
4 : ///
5 : /// This is a wrapper around platform-specific APIs.
6 1550 : pub fn rename_noreplace<P1: ?Sized + NixPath, P2: ?Sized + NixPath>(
7 1550 : src: &P1,
8 1550 : dst: &P2,
9 1550 : ) -> nix::Result<()> {
10 1550 : {
11 1550 : #[cfg(target_os = "linux")]
12 1550 : {
13 1550 : nix::fcntl::renameat2(
14 1550 : None,
15 1550 : src,
16 1550 : None,
17 1550 : dst,
18 1550 : nix::fcntl::RenameFlags::RENAME_NOREPLACE,
19 1550 : )
20 1550 : }
21 1550 : #[cfg(target_os = "macos")]
22 1550 : {
23 1550 : let res = src.with_nix_path(|src| {
24 1550 : dst.with_nix_path(|dst|
25 1550 : // SAFETY: `src` and `dst` are valid C strings as per the NixPath trait and they outlive the call to renamex_np.
26 1550 : unsafe {
27 1550 : nix::libc::renamex_np(src.as_ptr(), dst.as_ptr(), nix::libc::RENAME_EXCL)
28 1550 : })
29 1550 : })??;
30 1550 : nix::errno::Errno::result(res).map(drop)
31 1550 : }
32 1550 : #[cfg(not(any(target_os = "linux", target_os = "macos")))]
33 1550 : {
34 1550 : std::compile_error!("OS does not support no-replace renames");
35 1550 : }
36 1550 : }
37 1550 : }
38 :
39 : #[cfg(test)]
40 : mod test {
41 : use std::{fs, path::PathBuf};
42 :
43 : use super::*;
44 :
45 6 : fn testdir() -> camino_tempfile::Utf8TempDir {
46 6 : match crate::env::var("NEON_UTILS_RENAME_NOREPLACE_TESTDIR") {
47 0 : Some(path) => {
48 0 : let path: camino::Utf8PathBuf = path;
49 0 : camino_tempfile::tempdir_in(path).unwrap()
50 : }
51 6 : None => camino_tempfile::tempdir().unwrap(),
52 : }
53 6 : }
54 :
55 : #[test]
56 2 : fn test_absolute_paths() {
57 2 : let testdir = testdir();
58 2 : println!("testdir: {}", testdir.path());
59 2 :
60 2 : let src = testdir.path().join("src");
61 2 : let dst = testdir.path().join("dst");
62 2 :
63 2 : fs::write(&src, b"").unwrap();
64 2 : fs::write(&dst, b"").unwrap();
65 2 :
66 2 : let src = src.canonicalize().unwrap();
67 2 : assert!(src.is_absolute());
68 2 : let dst = dst.canonicalize().unwrap();
69 2 : assert!(dst.is_absolute());
70 :
71 2 : let result = rename_noreplace(&src, &dst);
72 2 : assert_eq!(result.unwrap_err(), nix::Error::EEXIST);
73 2 : }
74 :
75 : #[test]
76 2 : fn test_relative_paths() {
77 2 : let testdir = testdir();
78 2 : println!("testdir: {}", testdir.path());
79 2 :
80 2 : // this is fine because we run in nextest => process per test
81 2 : std::env::set_current_dir(testdir.path()).unwrap();
82 2 :
83 2 : let src = PathBuf::from("src");
84 2 : let dst = PathBuf::from("dst");
85 2 :
86 2 : fs::write(&src, b"").unwrap();
87 2 : fs::write(&dst, b"").unwrap();
88 2 :
89 2 : let result = rename_noreplace(&src, &dst);
90 2 : assert_eq!(result.unwrap_err(), nix::Error::EEXIST);
91 2 : }
92 :
93 : #[test]
94 2 : fn test_works_when_not_exists() {
95 2 : let testdir = testdir();
96 2 : println!("testdir: {}", testdir.path());
97 2 :
98 2 : let src = testdir.path().join("src");
99 2 : let dst = testdir.path().join("dst");
100 2 :
101 2 : fs::write(&src, b"content").unwrap();
102 2 :
103 2 : rename_noreplace(src.as_std_path(), dst.as_std_path()).unwrap();
104 2 : assert_eq!(
105 2 : "content",
106 2 : String::from_utf8(std::fs::read(&dst).unwrap()).unwrap()
107 2 : );
108 2 : }
109 : }
|