@@ -33,7 +33,7 @@ use std::{
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
-use tempfile::{NamedTempFile, TempDir};
+use tempfile::TempDir;
use text::LineEnding;
#[cfg(any(test, feature = "test-support"))]
@@ -525,49 +525,52 @@ impl Fs for RealFs {
Ok(bytes)
}
+ #[cfg(not(target_os = "windows"))]
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
let mut tmp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
// Use the directory of the destination as temp dir to avoid
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
// See https://github.com/zed-industries/zed/pull/8437 for more details.
- NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
- } else if cfg!(target_os = "windows") {
- // If temp dir is set to a different drive than the destination,
- // we receive error:
- //
- // failed to persist temporary file:
- // The system cannot move the file to a different disk drive. (os error 17)
- //
- // So we use the directory of the destination as a temp dir to avoid it.
- // https://github.com/zed-industries/zed/issues/16571
- NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
+ tempfile::NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
} else {
- NamedTempFile::new()
+ tempfile::NamedTempFile::new()
}?;
tmp_file.write_all(data.as_bytes())?;
+ tmp_file.persist(path)?;
+ Ok::<(), anyhow::Error>(())
+ })
+ .await?;
- let result = tmp_file.persist(&path);
- if cfg!(target_os = "windows") {
- // If file handle is already in used we receive error:
- //
- // failed to persist temporary file:
- // Access is denied. (os error 5)
- //
- // So we use direct fs write instead to avoid it.
- // https://github.com/zed-industries/zed/issues/30054
- if let Err(persist_err) = &result {
- if persist_err.error.raw_os_error() == Some(5) {
- return std::fs::write(&path, data.as_bytes()).map_err(Into::into);
- }
- }
- }
- result?;
+ Ok(())
+ }
+ #[cfg(target_os = "windows")]
+ async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
+ smol::unblock(move || {
+ // If temp dir is set to a different drive than the destination,
+ // we receive error:
+ //
+ // failed to persist temporary file:
+ // The system cannot move the file to a different disk drive. (os error 17)
+ //
+ // This is because `ReplaceFileW` does not support cross volume moves.
+ // See the remark section: "The backup file, replaced file, and replacement file must all reside on the same volume."
+ // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew#remarks
+ //
+ // So we use the directory of the destination as a temp dir to avoid it.
+ // https://github.com/zed-industries/zed/issues/16571
+ let temp_dir = TempDir::new_in(path.parent().unwrap_or(paths::temp_dir()))?;
+ let temp_file = {
+ let temp_file_path = temp_dir.path().join("temp_file");
+ let mut file = std::fs::File::create_new(&temp_file_path)?;
+ file.write_all(data.as_bytes())?;
+ temp_file_path
+ };
+ atomic_replace(path.as_path(), temp_file.as_path())?;
Ok::<(), anyhow::Error>(())
})
.await?;
-
Ok(())
}
@@ -2486,6 +2489,31 @@ async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
.await
}
+#[cfg(target_os = "windows")]
+fn atomic_replace<P: AsRef<Path>>(
+ replaced_file: P,
+ replacement_file: P,
+) -> windows::core::Result<()> {
+ use windows::{
+ Win32::Storage::FileSystem::{REPLACE_FILE_FLAGS, ReplaceFileW},
+ core::HSTRING,
+ };
+
+ // If the file does not exist, create it.
+ let _ = std::fs::File::create_new(replaced_file.as_ref());
+
+ unsafe {
+ ReplaceFileW(
+ &HSTRING::from(replaced_file.as_ref().to_string_lossy().to_string()),
+ &HSTRING::from(replacement_file.as_ref().to_string_lossy().to_string()),
+ None,
+ REPLACE_FILE_FLAGS::default(),
+ None,
+ None,
+ )
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -2905,4 +2933,37 @@ mod tests {
"B"
);
}
+
+ #[gpui::test]
+ async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
+ // With the file handle still open, the file should be replaced
+ // https://github.com/zed-industries/zed/issues/30054
+ let fs = RealFs {
+ git_binary_path: None,
+ executor,
+ };
+ let temp_dir = TempDir::new().unwrap();
+ let file_to_be_replaced = temp_dir.path().join("file.txt");
+ let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
+ file.write_all(b"Hello").unwrap();
+ // drop(file); // We still hold the file handle here
+ let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
+ assert_eq!(content, "Hello");
+ smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
+ let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
+ assert_eq!(content, "World");
+ }
+
+ #[gpui::test]
+ async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
+ let fs = RealFs {
+ git_binary_path: None,
+ executor,
+ };
+ let temp_dir = TempDir::new().unwrap();
+ let file_to_be_replaced = temp_dir.path().join("file.txt");
+ smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
+ let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
+ assert_eq!(content, "Hello");
+ }
}