pull/12985/merge
Yuki Kobayashi 2025-06-14 23:57:52 +03:00 committed by GitHub
commit e9019f233a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 122 additions and 63 deletions

View File

@ -68,6 +68,26 @@ pub enum Mode {
Insert = 2,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct BackupConfig {
pub kind: BackupConfigKind,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum BackupConfigKind {
#[default]
Auto,
Copy,
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum BackupKind {
Move,
Copy,
None,
}
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@ -984,6 +1004,7 @@ impl Document {
let encoding_with_bom_info = (self.encoding, self.has_bom);
let last_saved_time = self.last_saved_time;
let backup_config_kind = self.config.load().backup.kind;
// We encode the file according to the `Document`'s encoding.
let future = async move {
use tokio::fs;
@ -1027,38 +1048,58 @@ impl Document {
));
}
// Assume it is a hardlink to prevent data loss if the metadata cant be read (e.g. on certain Windows configurations)
let is_hardlink = helix_stdx::faccess::hardlink_count(&write_path).unwrap_or(2) > 1;
let backup = if path.exists() {
let path_ = write_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> {
let mut builder = tempfile::Builder::new();
builder.prefix(path_.file_name()?).suffix(".bck");
let backup_kind = {
match backup_config_kind {
BackupConfigKind::Copy => BackupKind::Copy,
BackupConfigKind::None => BackupKind::None,
BackupConfigKind::Auto => {
// Assume it is a hardlink to prevent data loss if the metadata cant be read (e.g. on certain Windows configurations)
let is_hardlink =
helix_stdx::faccess::hardlink_count(&write_path).unwrap_or(2) > 1;
let backup_path = if is_hardlink {
builder
.make_in(path_.parent()?, |backup| std::fs::copy(&path_, backup))
.ok()?
.into_temp_path()
} else {
builder
.make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup))
.ok()?
.into_temp_path()
};
if is_hardlink {
BackupKind::Copy
} else {
BackupKind::Move
}
}
}
};
backup_path.keep().ok()
})
.await
.ok()
.flatten()
} else {
None
let backup = match backup_kind {
BackupKind::Copy | BackupKind::Move if path.exists() => {
let path_ = write_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> {
let backup_path = tempfile::Builder::new()
.prefix(path_.file_name()?)
.suffix(".bck")
.make_in(path_.parent()?, |backup| {
match backup_kind {
BackupKind::Copy => {
std::fs::copy(&path_, backup)?;
}
BackupKind::Move => {
std::fs::rename(&path_, backup)?;
}
BackupKind::None => unreachable!(),
}
Ok(())
})
.ok()?
.into_temp_path();
backup_path.keep().ok()
})
.await
.ok()
.flatten()
}
_ => None,
};
let write_result: anyhow::Result<_> = async {
@ -1069,47 +1110,62 @@ impl Document {
}
.await;
let save_time = match fs::metadata(&write_path).await {
Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime),
Err(_) => SystemTime::now(),
};
let save_time = fs::metadata(&write_path)
.await
.and_then(|metadata| metadata.modified())
.unwrap_or_else(|_| SystemTime::now());
if let Some(backup) = backup {
if is_hardlink {
let mut delete = true;
if write_result.is_err() {
// Restore backup
let _ = tokio::fs::copy(&backup, &write_path).await.map_err(|e| {
delete = false;
log::error!("Failed to restore backup on write failure: {e}")
});
if let Err(write_err) = write_result {
if let Some(backup) = backup {
match backup_kind {
BackupKind::Copy => {
// Restore backup
if let Err(restore_err) = tokio::fs::copy(&backup, &write_path).await {
log::error!(
"Failed to restore backup on write failure: {restore_err}"
)
} else {
// successfully restored. delete backup
if let Err(delete_err) = tokio::fs::remove_file(backup).await {
log::error!(
"Failed to remove backup file on write: {delete_err}"
);
}
}
}
BackupKind::Move => {
// restore backup
if let Err(restore_err) = tokio::fs::rename(&backup, &write_path).await
{
log::error!(
"Failed to restore backup on write failure: {restore_err}"
);
}
}
BackupKind::None => unreachable!(),
}
if delete {
// Delete backup
let _ = tokio::fs::remove_file(backup)
.await
.map_err(|e| log::error!("Failed to remove backup file on write: {e}"));
}
} else if write_result.is_err() {
// restore backup
let _ = tokio::fs::rename(&backup, &write_path)
.await
.map_err(|e| log::error!("Failed to restore backup on write failure: {e}"));
} else {
// copy metadata and delete backup
log::error!(
"Failed to restore backup on write failure (backup doesn't exist) for write error: {write_err}"
);
}
} else if let Some(backup) = backup {
// backup exists & successfully saved. delete backup
if backup_kind == BackupKind::Move {
// the file is newly created one, therefore the metadata must be copied
let backup = backup.clone();
let _ = tokio::task::spawn_blocking(move || {
let _ = copy_metadata(&backup, &write_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}"));
if let Err(e) = copy_metadata(&backup, &write_path) {
log::error!("Failed to copy metadata on write: {e}");
}
})
.await;
}
if let Err(e) = tokio::fs::remove_file(backup).await {
log::error!("Failed to remove backup file on write: {e}");
}
}
write_result?;
let event = DocumentSavedEvent {
revision: current_rev,
save_time,

View File

@ -2,7 +2,8 @@ use crate::{
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
clipboard::ClipboardProvider,
document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
BackupConfig, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode,
SavePoint,
},
events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost},
graphics::{CursorKind, Rect},
@ -373,6 +374,7 @@ pub struct Config {
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
/// `true`.
pub editor_config: bool,
pub backup: BackupConfig,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -1031,6 +1033,7 @@ impl Default for Config {
end_of_line_diagnostics: DiagnosticFilter::Disable,
clipboard_provider: ClipboardProvider::default(),
editor_config: true,
backup: BackupConfig::default(),
}
}
}