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