use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use crate::platform;
use super::time_fmt::utc_timestamp_formatted;
#[cfg(unix)]
pub(crate) fn copy_content_to_target(target: &Path, content: &[u8]) -> Result<()> {
use std::io::Write;
let mut target_file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(target)
.with_context(|| format!("cannot open target for copy fallback: {}", target.display()))?;
target_file.write_all(content).with_context(|| {
format!(
"cannot write target for copy fallback: {}",
target.display()
)
})?;
platform::fsync_file(&target_file).with_context(|| {
format!(
"cannot flush target for copy fallback: {}",
target.display()
)
})?;
Ok(())
}
pub(crate) fn write_inplace_path(target: &Path, content: &[u8]) -> Result<bool> {
use std::io::Write;
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(false)
.open(target)
.with_context(|| {
format!(
"cannot open target for in-place write: {}",
target.display()
)
})?;
file.set_len(0)
.with_context(|| format!("ftruncate failed for {}", target.display()))?;
file.write_all(content)
.with_context(|| format!("in-place write failed for {}", target.display()))?;
file.flush()
.with_context(|| format!("in-place flush failed for {}", target.display()))?;
file.sync_data()
.with_context(|| format!("in-place sync failed for {}", target.display()))?;
Ok(false)
}
#[tracing::instrument(skip_all, fields(path = %target.display(), retention))]
pub(crate) fn create_backup(target: &Path, retention: u8) -> Result<std::path::PathBuf> {
create_backup_in(target, retention, None)
}
#[tracing::instrument(skip_all, fields(path = %target.display(), retention, output_dir))]
pub(crate) fn create_backup_in(
target: &Path,
retention: u8,
output_dir: Option<&Path>,
) -> Result<std::path::PathBuf> {
let now = utc_timestamp_formatted();
let filename = target.file_name().unwrap_or_default().to_string_lossy();
let backup_name = format!("{filename}{}{now}", crate::constants::BACKUP_INFIX);
let backup_path = match output_dir {
Some(dir) => {
if !dir.exists() {
fs::create_dir_all(dir).with_context(|| {
format!("cannot create backup output dir {}", dir.display())
})?;
}
dir.join(&backup_name)
}
None => target.with_file_name(&backup_name),
};
if backup_path.exists() {
crate::best_effort::best_effort(
std::fs::remove_file(&backup_path),
"remove stale backup before reflink",
);
}
reflink_copy::reflink_or_copy(target, &backup_path)
.with_context(|| format!("cannot create backup at {}", backup_path.display()))?;
let backup_file = fs::File::open(&backup_path)
.with_context(|| format!("cannot open backup for fsync: {}", backup_path.display()))?;
platform::fsync_file_best_effort(&backup_file);
if let Some(parent) = backup_path.parent() {
if let Err(e) = platform::fsync_dir(parent) {
tracing::warn!(
path = %parent.display(),
error = %e,
"fsync_dir after backup failed"
);
}
}
if retention > 0 {
cleanup_old_backups_in(
backup_path.parent().unwrap_or_else(|| Path::new(".")),
&filename,
retention,
);
}
Ok(backup_path)
}
pub(crate) fn delete_backup_quietly(path: &Path) {
match fs::remove_file(path) {
Ok(()) => {
tracing::debug!(path = %path.display(), "backup deleted after successful write");
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::debug!(
path = %path.display(),
"backup already gone (NotFound) — nothing to delete"
);
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"failed to delete backup after successful write — backup retained"
);
}
}
}
pub(crate) fn cleanup_old_backups_in(parent: &Path, prefix_name: &str, retention: u8) {
let prefix = format!("{prefix_name}{}", crate::constants::BACKUP_INFIX);
let mut backups: Vec<std::path::PathBuf> = match fs::read_dir(parent) {
Ok(entries) => entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name().and_then(|n| n.to_str()).is_some_and(|n| {
n.strip_prefix(&prefix).is_some_and(|stamp| {
!stamp.contains(crate::constants::BACKUP_INFIX)
&& crate::commands::is_backup_timestamp_name(n)
})
})
})
.collect(),
Err(_) => return,
};
if backups.len() <= retention as usize {
return;
}
backups.sort();
let to_remove = backups.len() - retention as usize;
for old in &backups[..to_remove] {
crate::best_effort::best_effort(fs::remove_file(old), "prune old backup");
}
}