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