use std::{
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
error::{Error, Result},
store,
};
pub fn backup(target: &Path, source_key: &str) -> Result<PathBuf> {
let session_dir = current_session_dir()?;
let dest = session_dir.join(source_key);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).map_err(|e| Error::io(parent, "create backup directory", e))?;
}
crate::fs::copy_file(target, &dest)?;
Ok(dest)
}
pub fn backup_dir(target: &Path, source_key: &str) -> Result<PathBuf> {
let session_dir = current_session_dir()?;
let dest = session_dir.join(source_key);
copy_dir_recursive(target, &dest)?;
Ok(dest)
}
pub fn list_sessions() -> Result<Vec<PathBuf>> {
let dir = store::backup_dir()?;
if !dir.exists() {
return Ok(Vec::new());
}
let mut sessions: Vec<PathBuf> = fs::read_dir(&dir)
.map_err(|e| Error::io(&dir, "read backup directory", e))?
.filter_map(std::result::Result::ok)
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
sessions.sort();
Ok(sessions)
}
pub fn clean(keep_last: Option<usize>, older_than_days: Option<u64>) -> Result<CleanSummary> {
let sessions = list_sessions()?;
let total = sessions.len();
let mut to_remove: Vec<&PathBuf> = Vec::new();
if let Some(keep) = keep_last {
let cutoff = total.saturating_sub(keep);
for s in sessions.iter().take(cutoff) {
if !to_remove.contains(&s) {
to_remove.push(s);
}
}
}
if let Some(days) = older_than_days {
let threshold_secs = days * 86_400;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
for s in &sessions {
let ts = session_timestamp_secs(s);
if ts > 0 && now.saturating_sub(ts) > threshold_secs && !to_remove.contains(&s) {
to_remove.push(s);
}
}
}
let removed_count = to_remove.len();
for path in to_remove {
fs::remove_dir_all(path).map_err(|e| Error::io(path, "remove backup session", e))?;
}
Ok(CleanSummary {
total,
removed: removed_count,
})
}
#[derive(Clone, Copy)]
pub struct CleanSummary {
pub total: usize,
pub removed: usize,
}
fn current_session_dir() -> Result<PathBuf> {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let dir = store::backup_dir()?.join(format!("{ts}"));
fs::create_dir_all(&dir).map_err(|e| Error::io(&dir, "create backup session directory", e))?;
Ok(dir)
}
fn session_timestamp_secs(path: &Path) -> u64 {
path.file_name()
.and_then(|n| n.to_str())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0)
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst).map_err(|e| Error::io(dst, "create directory", e))?;
for entry in fs::read_dir(src).map_err(|e| Error::io(src, "read directory", e))? {
let entry = entry.map_err(|e| Error::io(src, "read directory entry", e))?;
let sp = entry.path();
let dp = dst.join(entry.file_name());
if sp.is_dir() {
copy_dir_recursive(&sp, &dp)?;
} else {
crate::fs::copy_file(&sp, &dp)?;
}
}
Ok(())
}