summaryrefslogtreecommitdiff
path: root/rola-utils/functions/src/copy_with_temp_rename/sync.rs
blob: cdbe3db81ef733ee78dc53419cbf3371c06b86c4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
use std::borrow::Cow;
use std::io;
use std::path::{Path, PathBuf};

/// Safely copies a file: first copies to a temporary file, then atomically replaces the destination.
pub fn copy_with_temp_rename(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
    internal_copy_with_temp_rename(src.as_ref(), dst.as_ref())
}

fn internal_copy_with_temp_rename(src: &Path, dst: &Path) -> io::Result<()> {
    // Check if source file exists to avoid later failures
    if !src.exists() {
        return Err(io::Error::new(
            io::ErrorKind::NotFound,
            format!("source file does not exist: {}", src.display()),
        ));
    }

    // Canonicalize paths and compare; if same file, return early to avoid unnecessary copy
    // Only canonicalize destination if it exists; if it doesn't exist, skip the comparison
    let src_canonical = std::fs::canonicalize(src);
    if dst.exists() {
        let dst_canonical = std::fs::canonicalize(dst);
        match (src_canonical, dst_canonical) {
            (Ok(src_c), Ok(dst_c)) => {
                if src_c == dst_c {
                    return Ok(());
                }
            }
            (Err(e), _) | (_, Err(e)) => {
                return Err(io::Error::new(
                    io::ErrorKind::Other,
                    format!(
                        "failed to canonicalize paths (src: {}, dst: {}): {}",
                        src.display(),
                        dst.display(),
                        e
                    ),
                ));
            }
        }
    } else {
        // dst doesn't exist yet, so it cannot be the same file as src
        // If src canonicalization fails, propagate the error
        let _ = src_canonical.map_err(|e| {
            io::Error::new(
                io::ErrorKind::Other,
                format!(
                    "failed to canonicalize source path (src: {}): {}",
                    src.display(),
                    e
                ),
            )
        })?;
    }

    // Ensure both source and destination are regular files, preventing accidental operations on directories
    {
        let src_meta = src.metadata().map_err(|e| {
            io::Error::new(
                e.kind(),
                format!("failed to get source metadata for {}: {}", src.display(), e),
            )
        })?;
        if !src_meta.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("source is not a regular file: {}", src.display()),
            ));
        }

        if dst.exists() {
            let dst_meta = dst.metadata().map_err(|e| {
                io::Error::new(
                    e.kind(),
                    format!(
                        "failed to get destination metadata for {}: {}",
                        dst.display(),
                        e
                    ),
                )
            })?;
            if !dst_meta.is_file() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    format!(
                        "destination exists and is not a regular file: {}",
                        dst.display()
                    ),
                ));
            }
        }
    }

    // Ensure the parent directory of the destination exists, creating it if necessary
    if let Some(parent) = dst.parent() {
        std::fs::create_dir_all(parent)?;
    }

    // Create a temporary file in the destination directory as an intermediate write target
    let temp_dir = dst.parent().unwrap_or_else(|| Path::new("."));
    let base = src
        .file_name()
        .map(|f| f.to_string_lossy())
        .unwrap_or_else(|| Cow::Borrowed("temp"));
    let temp_path = atomic_create_temp(temp_dir, &base)?;

    // Copy source file contents to the temporary file; clean up on failure
    if let Err(e) = std::fs::copy(src, &temp_path) {
        let _ = std::fs::remove_file(&temp_path);
        return Err(e);
    }

    // Atomically replace the destination file; clean up temporary file on failure
    if let Err(e) = atomic_rename(&temp_path, dst) {
        let _ = std::fs::remove_file(&temp_path);
        return Err(e);
    }

    // Copy source file permissions to the destination
    let _ = std::fs::set_permissions(dst, src.metadata()?.permissions());

    Ok(())
}

/// Atomically renames a path, replacing the destination if it exists.
fn atomic_rename(src: &Path, dst: &Path) -> io::Result<()> {
    #[cfg(unix)]
    {
        std::fs::rename(src, dst)
    }

    #[cfg(not(unix))]
    {
        use std::ffi::OsStr;
        use std::os::windows::ffi::OsStrExt;

        // Convert OsStr to a null-terminated UTF-16 vector
        fn to_wide_null(os_str: &OsStr) -> Vec<u16> {
            os_str.encode_wide().chain(Some(0)).collect()
        }

        let src_wide = to_wide_null(src.as_os_str());
        let dst_wide = to_wide_null(dst.as_os_str());

        // SAFETY: `src_wide` and `dst_wide` are valid, owned `Vec<u16>`s,
        // the pointers returned by `as_ptr()` are valid for their lifetime and are null-terminated.
        // Declares the Windows API MoveFileExW for atomic renaming.
        unsafe extern "system" {
            /// MOVEFILE_REPLACE_EXISTING = 1
            fn MoveFileExW(
                lpExistingFileName: *const u16,
                lpNewFileName: *const u16,
                dwFlags: u32,
            ) -> i32;
        }

        const MOVEFILE_REPLACE_EXISTING: u32 = 1;

        // SAFETY:
        // 1. `src_wide` and `dst_wide` are null-terminated UTF-16 strings, valid for their lifetime.
        // 2. `MoveFileExW` is a Windows system API declared via extern "system",
        //    with correct calling convention and ABI.
        // 3. Return value is checked; errors are handled via `last_os_error()`.
        let ret = unsafe {
            MoveFileExW(
                src_wide.as_ptr(),
                dst_wide.as_ptr(),
                MOVEFILE_REPLACE_EXISTING,
            )
        };

        if ret == 0 {
            Err(io::Error::last_os_error())
        } else {
            Ok(())
        }
    }
}

fn atomic_create_temp(dir: &Path, base: &str) -> io::Result<PathBuf> {
    use std::fs::OpenOptions;

    // Generate temporary file names; first attempt without index, then incrementing to avoid conflicts
    let make_name = |i: usize| {
        let name = if i == 0 {
            format!(".temp_{}", base)
        } else {
            format!(".temp_{}_{}", base, i)
        };
        dir.join(name)
    };

    const MAX_ATTEMPTS: usize = 10000;
    // Loop to create a new file; create_new ensures the file does not already exist
    for i in 0..MAX_ATTEMPTS {
        let path = make_name(i);
        match OpenOptions::new().create_new(true).write(true).open(&path) {
            Ok(_) => return Ok(path),
            Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
            Err(e) => return Err(e),
        }
    }

    Err(io::Error::new(
        io::ErrorKind::Other,
        "exceeded max attempts to create temp file",
    ))
}