Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/document.rs')
| -rw-r--r-- | helix-view/src/document.rs | 87 |
1 files changed, 60 insertions, 27 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index da68a67f..0c3a75f1 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -10,6 +10,7 @@ use helix_core::encoding::Encoding; use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::text_annotations::{InlineAnnotation, Overlay}; use helix_lsp::util::lsp_pos_to_pos; +use helix_stdx::faccess::{copy_metadata, readonly}; use helix_vcs::{DiffHandle, DiffProviderRegistry}; use ::parking_lot::Mutex; @@ -870,7 +871,7 @@ impl Document { // We encode the file according to the `Document`'s encoding. let future = async move { - use tokio::{fs, fs::File}; + use tokio::fs; if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created if !parent.exists() { @@ -893,8 +894,63 @@ impl Document { } } - let mut file = File::create(&path).await?; - to_writer(&mut file, encoding_with_bom_info, &text).await?; + if readonly(&path) { + bail!(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Path is read only" + )); + } + let backup = if path.exists() { + let path_ = path.clone(); + // hacks: we use tempfile to handle the complex task of creating + // non clobbered temporary path for us we don't want + // the whole automatically delete path on drop thing + // since the path doesn't exist yet, we just want + // the path + tokio::task::spawn_blocking(move || -> Option<PathBuf> { + tempfile::Builder::new() + .prefix(path_.file_name()?) + .suffix(".bck") + .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup)) + .ok()? + .into_temp_path() + .keep() + .ok() + }) + .await + .ok() + .flatten() + } else { + None + }; + + let write_result: anyhow::Result<_> = async { + let mut dst = tokio::fs::File::create(&path).await?; + to_writer(&mut dst, encoding_with_bom_info, &text).await?; + Ok(()) + } + .await; + + if let Some(backup) = backup { + if write_result.is_err() { + // restore backup + let _ = tokio::fs::rename(&backup, &path) + .await + .map_err(|e| log::error!("Failed to restore backup on write failure: {e}")); + } else { + // copy metadata and delete backup + let path_ = path.clone(); + let _ = tokio::task::spawn_blocking(move || { + let _ = copy_metadata(&backup, &path_) + .map_err(|e| log::error!("Failed to copy metadata on write: {e}")); + let _ = std::fs::remove_file(backup) + .map_err(|e| log::error!("Failed to remove backup file on write: {e}")); + }) + .await; + } + } + + write_result?; let event = DocumentSavedEvent { revision: current_rev, @@ -955,35 +1011,12 @@ impl Document { } } - #[cfg(unix)] // Detect if the file is readonly and change the readonly field if necessary (unix only) pub fn detect_readonly(&mut self) { - use rustix::fs::{access, Access}; // Allows setting the flag for files the user cannot modify, like root files self.readonly = match &self.path { None => false, - Some(p) => match access(p, Access::WRITE_OK) { - Ok(_) => false, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, - Err(_) => true, - }, - }; - } - - #[cfg(not(unix))] - // Detect if the file is readonly and change the readonly field if necessary (non-unix os) - pub fn detect_readonly(&mut self) { - // TODO Use the Windows' function `CreateFileW` to check if a file is readonly - // Discussion: https://github.com/helix-editor/helix/pull/7740#issuecomment-1656806459 - // Vim implementation: https://github.com/vim/vim/blob/4c0089d696b8d1d5dc40568f25ea5738fa5bbffb/src/os_win32.c#L7665 - // Windows binding: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/FileSystem/fn.CreateFileW.html - self.readonly = match &self.path { - None => false, - Some(p) => match std::fs::metadata(p) { - Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, - Err(_) => false, - Ok(metadata) => metadata.permissions().readonly(), - }, + Some(p) => readonly(p), }; } |