diff --git a/crates/platform/src/staging.rs b/crates/platform/src/staging.rs index 2cee86c5e..fee0a1633 100644 --- a/crates/platform/src/staging.rs +++ b/crates/platform/src/staging.rs @@ -99,7 +99,8 @@ pub fn staging_suffixes() -> impl Iterator { /// a folder at the target is refused by the rename. Flushing is the staging file's own /// (`sync_all`); parent-directory durability after a power loss is not promised. On Unix a file /// that is replaced keeps its permissions, so a private file stays private; a new file gets the -/// usual mode for a new file. +/// usual mode for a new file. On Windows and macOS a replaced file keeps its creation date, where +/// the volume stores one. pub fn write_atomically(path: &Path, bytes: &[u8], form: StagingName) -> std::io::Result<()> { write_atomically_with(path, bytes, form, staging_suffixes(), std::fs::File::sync_all) } @@ -128,12 +129,17 @@ pub fn write_atomically_with( let keep = std::fs::metadata(path).ok().map(|m| m.permissions()); #[cfg(not(unix))] let keep = None; + // A saved file is the same document: it keeps its creation date (#2658). + #[cfg(any(windows, target_os = "macos"))] + let created = std::fs::metadata(path).ok().filter(|m| m.is_file()).and_then(|m| m.created().ok()); let (tmp, file) = create_staging_as(dir, name, form, suffixes, keep.as_ref())?; let result = (|| { // Closed at the end of the block, before the rename (required on Windows). { let mut file = file; file.write_all(bytes)?; + #[cfg(any(windows, target_os = "macos"))] + keep_created(&file, created); sync_staged(&file, sync_all)?; } std::fs::rename(&tmp, path) @@ -144,6 +150,20 @@ pub fn write_atomically_with( result } +/// Give the staging file the creation date of the file it replaces, where the system keeps one +/// that a program can set (Windows, macOS). Best effort: a volume that can't store it leaves the +/// new file's own date, as before. +#[cfg(any(windows, target_os = "macos"))] +fn keep_created(file: &std::fs::File, created: Option) { + if let Some(created) = created { + #[cfg(target_os = "macos")] + use std::os::macos::fs::FileTimesExt as _; + #[cfg(windows)] + use std::os::windows::fs::FileTimesExt as _; + let _ = file.set_times(std::fs::FileTimes::new().set_created(created)); + } +} + /// Flush the staging file to the disk before it replaces the target. On macOS `sync_all` asks /// for a full flush (`F_FULLFSYNC`), which SMB and FUSE volumes, such as a mounted Cryptomator /// vault, refuse as unsupported: there a plain `fsync` is the strongest flush the volume offers. @@ -313,4 +333,32 @@ mod tests { assert_eq!(mode(&fresh), mode(&plain), "a new file is not made private"); let _ = std::fs::remove_dir_all(&dir); } + + /// #2658: a save replaces the file, but the document is the same one: it keeps its creation + /// date where the system lets a program set it. + #[cfg(any(windows, target_os = "macos"))] + #[test] + fn a_replaced_file_keeps_its_creation_date() { + #[cfg(target_os = "macos")] + use std::os::macos::fs::FileTimesExt as _; + #[cfg(windows)] + use std::os::windows::fs::FileTimesExt as _; + let dir = test_dir("created"); + let target = dir.join("doc.pdf"); + std::fs::write(&target, b"OLD").unwrap(); + // 2020-01-02 03:04:05 UTC. + let then = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_577_934_245); + let file = std::fs::OpenOptions::new().write(true).open(&target).unwrap(); + file.set_times(std::fs::FileTimes::new().set_created(then)).unwrap(); + drop(file); + assert_eq!(std::fs::metadata(&target).unwrap().created().unwrap(), then, "the fixture's date is set"); + write_atomically(&target, b"NEW", StagingName::SuffixThenTag).unwrap(); + assert_eq!(std::fs::read(&target).unwrap(), b"NEW"); + assert_eq!(std::fs::metadata(&target).unwrap().created().unwrap(), then, "the saved file keeps its creation date"); + // A new file gets a date of its own. + let fresh = dir.join("new.pdf"); + write_atomically(&fresh, b"NEW", StagingName::SuffixThenTag).unwrap(); + assert!(std::fs::metadata(&fresh).unwrap().created().unwrap() > then); + let _ = std::fs::remove_dir_all(&dir); + } }