|
|
@ -1,6 +1,7 @@
|
|
|
|
use anyhow::{anyhow, bail, Error};
|
|
|
|
use anyhow::{anyhow, bail, Error};
|
|
|
|
use arc_swap::access::{Access, DynAccess};
|
|
|
|
use arc_swap::access::DynAccess;
|
|
|
|
use arc_swap::ArcSwap;
|
|
|
|
use arc_swap::ArcSwap;
|
|
|
|
|
|
|
|
use filetime::FileTime;
|
|
|
|
use futures_util::future::BoxFuture;
|
|
|
|
use futures_util::future::BoxFuture;
|
|
|
|
use futures_util::FutureExt;
|
|
|
|
use futures_util::FutureExt;
|
|
|
|
use helix_core::auto_pairs::AutoPairs;
|
|
|
|
use helix_core::auto_pairs::AutoPairs;
|
|
|
@ -23,7 +24,6 @@ use std::collections::HashMap;
|
|
|
|
use std::fmt::Display;
|
|
|
|
use std::fmt::Display;
|
|
|
|
use std::future::Future;
|
|
|
|
use std::future::Future;
|
|
|
|
use std::io;
|
|
|
|
use std::io;
|
|
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::sync::{Arc, Weak};
|
|
|
|
use std::sync::{Arc, Weak};
|
|
|
@ -153,7 +153,6 @@ impl Backup {
|
|
|
|
// - it is a hardlink
|
|
|
|
// - it is a hardlink
|
|
|
|
// - it is a symlink
|
|
|
|
// - it is a symlink
|
|
|
|
// - we don't have file create perms for the dir
|
|
|
|
// - we don't have file create perms for the dir
|
|
|
|
// TODO: also set copy when perms can't be set or metadata can't be read
|
|
|
|
|
|
|
|
if !copy {
|
|
|
|
if !copy {
|
|
|
|
// Conservatively assume it is a hardlink if we can't read metadata
|
|
|
|
// Conservatively assume it is a hardlink if we can't read metadata
|
|
|
|
let is_hardlink = {
|
|
|
|
let is_hardlink = {
|
|
|
@ -165,16 +164,16 @@ impl Backup {
|
|
|
|
copy = true;
|
|
|
|
copy = true;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// Check if we have write permissions by creating a temporary file
|
|
|
|
// Check if we have write permissions by creating a temporary file
|
|
|
|
|
|
|
|
let from_meta = tokio::fs::metadata(&p).await?;
|
|
|
|
|
|
|
|
let perms = from_meta.permissions();
|
|
|
|
let mut builder = tempfile::Builder::new();
|
|
|
|
let mut builder = tempfile::Builder::new();
|
|
|
|
// TODO: Need a way to create cross-platform perms with more granularity
|
|
|
|
builder.permissions(perms);
|
|
|
|
// builder.permissions()
|
|
|
|
|
|
|
|
if let Ok(f) = builder.tempfile() {
|
|
|
|
if let Ok(f) = builder.tempfile() {
|
|
|
|
// Check if we have perms to set perms
|
|
|
|
// Check if we have perms to set perms
|
|
|
|
#[cfg(unix)]
|
|
|
|
#[cfg(unix)]
|
|
|
|
{
|
|
|
|
{
|
|
|
|
use std::os::{fd::AsFd, unix::fs::MetadataExt};
|
|
|
|
use std::os::{fd::AsFd, unix::fs::MetadataExt};
|
|
|
|
|
|
|
|
|
|
|
|
let from_meta = tokio::fs::metadata(&p).await?;
|
|
|
|
|
|
|
|
let to_meta = tokio::fs::metadata(&f.path()).await?;
|
|
|
|
let to_meta = tokio::fs::metadata(&f.path()).await?;
|
|
|
|
let _ = fchown(
|
|
|
|
let _ = fchown(
|
|
|
|
f.as_file().as_fd(),
|
|
|
|
f.as_file().as_fd(),
|
|
|
@ -205,12 +204,14 @@ impl Backup {
|
|
|
|
// - directory is not writable
|
|
|
|
// - directory is not writable
|
|
|
|
// - path is a directory
|
|
|
|
// - path is a directory
|
|
|
|
// - path exists
|
|
|
|
// - path exists
|
|
|
|
let mut dir_exists = false;
|
|
|
|
|
|
|
|
let escaped_p = helix_stdx::path::escape_path(&p);
|
|
|
|
let escaped_p = helix_stdx::path::escape_path(&p);
|
|
|
|
|
|
|
|
// `.join` on absolute path replaces instead of append
|
|
|
|
|
|
|
|
debug_assert!(escaped_p.is_relative());
|
|
|
|
|
|
|
|
|
|
|
|
'outer: for dir in config.directories.iter().filter(|p| p.is_dir()) {
|
|
|
|
'outer: for dir in config.directories.iter().filter(|p| p.is_dir()) {
|
|
|
|
let ext = config.extension.as_str();
|
|
|
|
let ext = config.extension.as_str();
|
|
|
|
let bck_base_path = &dir.join(&escaped_p);
|
|
|
|
let bck_base_path = &dir.join(&escaped_p);
|
|
|
|
let mut backup = bck_base_path.join(&ext);
|
|
|
|
let mut backup = bck_base_path.with_extension(&ext);
|
|
|
|
|
|
|
|
|
|
|
|
// NOTE: Should we just overwrite regardless?
|
|
|
|
// NOTE: Should we just overwrite regardless?
|
|
|
|
// If the backup file already exists, we'll try to add a number before the extension
|
|
|
|
// If the backup file already exists, we'll try to add a number before the extension
|
|
|
@ -218,10 +219,11 @@ impl Backup {
|
|
|
|
// NOTE: u8 since if we need more than 256, there might be an issue
|
|
|
|
// NOTE: u8 since if we need more than 256, there might be an issue
|
|
|
|
let mut n: u8 = 1;
|
|
|
|
let mut n: u8 = 1;
|
|
|
|
while backup.exists() {
|
|
|
|
while backup.exists() {
|
|
|
|
backup = bck_base_path.join(n.to_string()).join(&ext);
|
|
|
|
backup = bck_base_path.with_extension(format!("{n}.{ext}"));
|
|
|
|
let Some(n) = n.checked_add(1) else {
|
|
|
|
if n == u8::MAX {
|
|
|
|
continue 'outer;
|
|
|
|
continue 'outer;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
n = n + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if copy {
|
|
|
|
if copy {
|
|
|
@ -233,7 +235,7 @@ impl Backup {
|
|
|
|
{
|
|
|
|
{
|
|
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
|
|
|
|
|
|
|
|
|
|
let mut from_meta = tokio::fs::metadata(&p).await?;
|
|
|
|
let from_meta = tokio::fs::metadata(&p).await?;
|
|
|
|
let mut perms = from_meta.permissions();
|
|
|
|
let mut perms = from_meta.permissions();
|
|
|
|
|
|
|
|
|
|
|
|
// Strip s-bit
|
|
|
|
// Strip s-bit
|
|
|
@ -249,7 +251,11 @@ impl Backup {
|
|
|
|
perms.set_mode(new_perms);
|
|
|
|
perms.set_mode(new_perms);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
std::fs::set_permissions(&backup, perms)?;
|
|
|
|
std::fs::set_permissions(&backup, perms)?;
|
|
|
|
// TODO: Set time
|
|
|
|
|
|
|
|
|
|
|
|
let atime = FileTime::from_last_access_time(&from_meta);
|
|
|
|
|
|
|
|
let mtime = FileTime::from_last_modification_time(&from_meta);
|
|
|
|
|
|
|
|
filetime::set_file_times(&backup, atime, mtime)?;
|
|
|
|
|
|
|
|
|
|
|
|
copy_xattr(&p, &backup)?;
|
|
|
|
copy_xattr(&p, &backup)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -260,25 +266,15 @@ impl Backup {
|
|
|
|
copy_ownership(&p, &backup_)?;
|
|
|
|
copy_ownership(&p, &backup_)?;
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
.await??;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(Self {
|
|
|
|
|
|
|
|
copy: true,
|
|
|
|
|
|
|
|
path: backup,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
tokio::fs::rename(p, &backup).await?;
|
|
|
|
tokio::fs::rename(p, &backup).await?;
|
|
|
|
return Ok(Self {
|
|
|
|
|
|
|
|
copy: false,
|
|
|
|
|
|
|
|
path: backup,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(Self { copy, path: backup });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: Try to initialize last dir if none of the dirs exist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
bail!("Could not write into a backup directory");
|
|
|
|
bail!("err");
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -1070,19 +1066,8 @@ impl Document {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let write_path = tokio::fs::read_link(&path)
|
|
|
|
|
|
|
|
.await
|
|
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
|
|
.and_then(|p| {
|
|
|
|
|
|
|
|
if p.is_relative() {
|
|
|
|
|
|
|
|
path.parent().map(|parent| parent.join(p))
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
Some(p)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.unwrap_or_else(|| path.clone());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if readonly(&write_path) {
|
|
|
|
if readonly(&path) {
|
|
|
|
bail!(std::io::Error::new(
|
|
|
|
bail!(std::io::Error::new(
|
|
|
|
std::io::ErrorKind::PermissionDenied,
|
|
|
|
std::io::ErrorKind::PermissionDenied,
|
|
|
|
"Path is read only"
|
|
|
|
"Path is read only"
|
|
|
@ -1090,61 +1075,72 @@ impl Document {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use a backup file
|
|
|
|
// Use a backup file
|
|
|
|
|
|
|
|
let meta = tokio::fs::metadata(&path).await?;
|
|
|
|
let mut bck = None;
|
|
|
|
let mut bck = None;
|
|
|
|
let write_result = if bck_config.kind != crate::editor::BackupKind::None {
|
|
|
|
let write_result: Result<(), Error> = async {
|
|
|
|
bck = Some(Backup::from(write_path.clone(), &bck_config).await?);
|
|
|
|
if path.exists() && bck_config.kind != crate::editor::BackupKind::None {
|
|
|
|
let bck = bck.unwrap();
|
|
|
|
match Backup::from(path.clone(), &bck_config).await {
|
|
|
|
|
|
|
|
Ok(b) => bck = Some(b),
|
|
|
|
// SECURITY: Ensure that the created file has the same perms as the original file
|
|
|
|
Err(_) if force => {}
|
|
|
|
let dst = if !bck.copy {
|
|
|
|
Err(e) => bail!("Could not create backup: {e}"),
|
|
|
|
let from_meta = tokio::fs::metadata(&write_path).await?;
|
|
|
|
|
|
|
|
let mut open_opt = tokio::fs::OpenOptions::new()
|
|
|
|
|
|
|
|
.read(true)
|
|
|
|
|
|
|
|
.write(true)
|
|
|
|
|
|
|
|
.create_new(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|
|
|
let mode = from_meta.permissions().mode();
|
|
|
|
|
|
|
|
open_opt.mode(mode);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let file = open_opt.open(&bck.path).await?;
|
|
|
|
if let Some(ref bck) = bck {
|
|
|
|
let to_meta = file.metadata().await?;
|
|
|
|
// SECURITY: Ensure that the created file has the same perms as the original file
|
|
|
|
|
|
|
|
let mut dst = if !bck.copy {
|
|
|
|
|
|
|
|
let mut open_opt = tokio::fs::OpenOptions::new();
|
|
|
|
|
|
|
|
open_opt.read(true).write(true).create_new(true);
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
#[cfg(unix)]
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// TODO: set gid/uid via fchown
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
}
|
|
|
|
let mode = meta.permissions().mode();
|
|
|
|
|
|
|
|
open_opt.mode(mode);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
let file = open_opt.open(&path).await?;
|
|
|
|
{
|
|
|
|
|
|
|
|
let from = write_path.clone();
|
|
|
|
|
|
|
|
let to = bck.path.clone();
|
|
|
|
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
|
|
|
|
helix_stdx::faccess::copy_ownership(&from, &to)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
file
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// SECURITY: Backup copy already exists
|
|
|
|
|
|
|
|
tokio::fs::File::create(&write_path).await?
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
|
|
|
|
#[cfg(unix)]
|
|
|
|
dst.sync_all().await?;
|
|
|
|
{
|
|
|
|
Ok(())
|
|
|
|
use std::os::fd::AsFd;
|
|
|
|
} else {
|
|
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
let dst = tokio::fs::File::create(&write_path).await?;
|
|
|
|
helix_stdx::faccess::fchown(
|
|
|
|
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
|
|
|
|
file.as_fd(),
|
|
|
|
dst.sync_all().await?;
|
|
|
|
Some(meta.uid()),
|
|
|
|
Ok(())
|
|
|
|
Some(meta.gid()),
|
|
|
|
};
|
|
|
|
)?;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
let from = path.clone();
|
|
|
|
|
|
|
|
let to = bck.path.clone();
|
|
|
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), Error> {
|
|
|
|
|
|
|
|
helix_stdx::faccess::copy_ownership(&from, &to)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.await??;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
file
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// SECURITY: Backup copy already exists
|
|
|
|
|
|
|
|
tokio::fs::File::create(&path).await?
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let save_time = match fs::metadata(&write_path).await {
|
|
|
|
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
|
|
|
|
|
|
|
|
dst.sync_all().await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
let mut dst = tokio::fs::File::create(&path).await?;
|
|
|
|
|
|
|
|
to_writer(&mut dst, encoding_with_bom_info, &text).await?;
|
|
|
|
|
|
|
|
dst.sync_all().await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let save_time = match fs::metadata(&path).await {
|
|
|
|
Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime),
|
|
|
|
Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime),
|
|
|
|
Err(_) => SystemTime::now(),
|
|
|
|
Err(_) => SystemTime::now(),
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -1158,35 +1154,41 @@ impl Document {
|
|
|
|
let mut delete_bck = true;
|
|
|
|
let mut delete_bck = true;
|
|
|
|
if write_result.is_err() {
|
|
|
|
if write_result.is_err() {
|
|
|
|
// If original file no longer exists, then backup is renamed to original file
|
|
|
|
// If original file no longer exists, then backup is renamed to original file
|
|
|
|
if !write_path.exists() {
|
|
|
|
if !path.exists() {
|
|
|
|
if !tokio::fs::rename(&bck.path, &write_path)
|
|
|
|
delete_bck = false;
|
|
|
|
|
|
|
|
if tokio::fs::rename(&bck.path, &path)
|
|
|
|
.await
|
|
|
|
.await
|
|
|
|
.map_err(|e| {
|
|
|
|
.map_err(|e| {
|
|
|
|
delete_bck = false;
|
|
|
|
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.is_err()
|
|
|
|
.is_ok()
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// TODO: Set timestamps to prior to write
|
|
|
|
// Reset timestamps
|
|
|
|
|
|
|
|
let atime = FileTime::from_last_access_time(&meta);
|
|
|
|
|
|
|
|
let mtime = FileTime::from_last_modification_time(&meta);
|
|
|
|
|
|
|
|
filetime::set_file_times(&path, atime, mtime)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
if bck.copy {
|
|
|
|
if bck.copy {
|
|
|
|
// Restore backup from copy
|
|
|
|
// Restore backup from copy
|
|
|
|
let _ = tokio::fs::copy(&bck.path, &write_path).await.map_err(|e| {
|
|
|
|
let _ = tokio::fs::copy(&bck.path, &path).await.map_err(|e| {
|
|
|
|
delete_bck = false;
|
|
|
|
delete_bck = false;
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// restore backup
|
|
|
|
// restore backup
|
|
|
|
let _ = tokio::fs::rename(&bck.path, &write_path)
|
|
|
|
let _ = tokio::fs::rename(&bck.path, &path).await.map_err(|e| {
|
|
|
|
.await
|
|
|
|
delete_bck = false;
|
|
|
|
.map_err(|e| {
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
delete_bck = false;
|
|
|
|
});
|
|
|
|
log::error!("Failed to restore backup on write failure: {e}")
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Delete backup if we're done with it
|
|
|
|
|
|
|
|
if delete_bck {
|
|
|
|
|
|
|
|
tokio::fs::remove_file(bck.path).await?;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
write_result?;
|
|
|
|
write_result?;
|
|
|
|