From 6fd49d656597724d8af024a38c7c67af604c0a2a Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 05:08:02 +0800 Subject: [PATCH] M1: a saved file keeps its creation date (#2658) (#3291) * M1: a saved file keeps its creation date (#2658) Every save writes a staging file and renames it over the target, so the file's creation date became the save time. Before the rename the staging file now takes the replaced file's creation date, where the system lets a program set one (Windows and macOS, through std's FileTimesExt; best effort). A new file keeps its own date. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q * Staging: compile the creation-date step only where it applies; say so in write_atomically's doc Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/platform/src/staging.rs | 50 +++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) 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); + } }