use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use chrono::Local;
use super::error::AutosaveResult;
use super::format::CompatMode;
use super::save_file::{SaveEntry, validate_save_file, write_save_file_with_mode};
#[derive(Debug, Clone)]
pub struct BackupConfig {
pub enable_savb: bool,
pub num_seq_files: usize,
pub seq_period: Duration,
pub enable_dated: bool,
pub dated_interval: Duration,
}
impl Default for BackupConfig {
fn default() -> Self {
Self {
enable_savb: true,
num_seq_files: 3,
seq_period: Duration::from_secs(60),
enable_dated: false,
dated_interval: Duration::from_secs(3600),
}
}
}
pub(super) async fn publish_copy(src: &Path, dst: &Path) -> AutosaveResult<()> {
let mut tmp = dst.as_os_str().to_os_string();
tmp.push(".tmp");
let tmp = PathBuf::from(tmp);
let src = src.to_path_buf();
let dst = dst.to_path_buf();
let published = crate::runtime::fs::blocking({
let (src, dst, tmp) = (src.clone(), dst.clone(), tmp.clone());
move || {
let published = std::fs::copy(&src, &tmp).and_then(|_| std::fs::rename(&tmp, &dst));
if published.is_err() {
let _ = std::fs::remove_file(&tmp);
}
published
}
})
.await;
if let Err(ref e) = published {
crate::runtime::log::errlog_printf(&format!(
"autosave: backup {} -> {} not written: {e}\n",
src.display(),
dst.display()
));
}
published?;
Ok(())
}
#[derive(Debug, Default)]
pub struct BackupState {
last_seq_time: Option<std::time::Instant>,
last_dated_time: Option<std::time::Instant>,
seq_index: usize,
}
impl BackupState {
async fn rotate_seq(&mut self, sav_path: &Path, config: &BackupConfig) -> AutosaveResult<()> {
if config.num_seq_files == 0 {
return Ok(());
}
if self
.last_seq_time
.is_some_and(|t| t.elapsed() < config.seq_period)
{
return Ok(());
}
let ext = format!("sav{}", self.seq_index);
publish_copy(sav_path, &sav_path.with_extension(&ext)).await?;
self.seq_index = (self.seq_index + 1) % config.num_seq_files;
self.last_seq_time = Some(std::time::Instant::now());
Ok(())
}
async fn write_dated(&mut self, sav_path: &Path, config: &BackupConfig) -> AutosaveResult<()> {
if !config.enable_dated {
return Ok(());
}
if self
.last_dated_time
.is_some_and(|t| t.elapsed() < config.dated_interval)
{
return Ok(());
}
let timestamp = Local::now().format("%y%m%d-%H%M%S");
let ext = format!("sav_{timestamp}");
publish_copy(sav_path, &sav_path.with_extension(&ext)).await?;
self.last_dated_time = Some(std::time::Instant::now());
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SavbState {
Disabled,
Ok,
Rewritten,
}
async fn ensure_savb(
sav_path: &Path,
config: &BackupConfig,
entries: &[SaveEntry],
compat: CompatMode,
) -> AutosaveResult<SavbState> {
if !config.enable_savb {
return Ok(SavbState::Disabled);
}
let savb = sav_path.with_extension("savB");
let state = validate_save_file(&savb).await;
if matches!(state, Ok(true)) {
return Ok(SavbState::Ok);
}
crate::runtime::log::errlog_printf(&format!(
"autosave: backup file ({}) bad or not found. Writing a new one.\n",
savb.display()
));
if matches!(state, Ok(false)) {
let mut aside = savb.as_os_str().to_os_string();
aside.push(format!("_SBAD_{}", Local::now().format("%y%m%d-%H%M%S")));
let _ = publish_copy(&savb, Path::new(&aside)).await;
}
if let Err(e) = write_save_file_with_mode(&savb, entries, compat).await {
crate::runtime::log::errlog_printf(&format!(
"autosave: backup {} not written: {e}\n",
savb.display()
));
return Err(e);
}
Ok(SavbState::Rewritten)
}
pub async fn rotate_backups(
sav_path: &Path,
config: &BackupConfig,
state: &mut BackupState,
entries: &[SaveEntry],
compat: CompatMode,
) -> AutosaveResult<SavbState> {
let savb_state = ensure_savb(sav_path, config, entries, compat).await?;
if sav_path.exists() && validate_save_file(sav_path).await.unwrap_or(false) {
state.rotate_seq(sav_path, config).await?;
state.write_dated(sav_path, config).await?;
}
Ok(savb_state)
}
pub async fn publish_savb(sav_path: &Path, savb_state: SavbState) -> AutosaveResult<()> {
if savb_state != SavbState::Ok {
return Ok(());
}
publish_copy(sav_path, &sav_path.with_extension("savB")).await
}
async fn modified_time(path: &Path) -> Option<SystemTime> {
let path = path.to_path_buf();
crate::runtime::fs::blocking(move || std::fs::metadata(&path)?.modified())
.await
.ok()
}
pub async fn find_best_save_file(base_path: &Path, config: &BackupConfig) -> Option<PathBuf> {
if let Ok(true) = validate_save_file(base_path).await {
return Some(base_path.to_path_buf());
}
if config.enable_savb {
let savb = base_path.with_extension("savB");
if let Ok(true) = validate_save_file(&savb).await {
return Some(savb);
}
}
let mut newest: Option<(Option<SystemTime>, PathBuf)> = None;
for i in 0..config.num_seq_files {
let ext = format!("sav{i}");
let seq_path = base_path.with_extension(&ext);
if !matches!(validate_save_file(&seq_path).await, Ok(true)) {
continue;
}
let mtime = modified_time(&seq_path).await;
if newest.as_ref().is_none_or(|(best, _)| mtime > *best) {
newest = Some((mtime, seq_path));
}
}
newest.map(|(_, path)| path)
}