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_tempfile_to_target(_temp: &std::fs::File, 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).ok();
let _ = target_file;
if let Some(parent) = target.parent() {
if let Ok(entries) = fs::read_dir(parent) {
for entry in entries.flatten() {
let name = entry.file_name();
if let Some(name) = name.to_str() {
if name.starts_with(crate::constants::TEMPFILE_PREFIX) {
let _ = fs::remove_file(entry.path());
}
}
}
}
}
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()))?;
let _ = file.sync_data();
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}.bak.{now}");
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() {
let _ = std::fs::remove_file(&backup_path);
}
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}.bak.");
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.starts_with(&prefix))
})
.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] {
let _ = fs::remove_file(old);
}
}