use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use async_trait::async_trait;
use tracing::debug;
use super::save_session_command::SaveSessionCommand;
use crate::engine::command::{Command, CommandStatus};
use crate::error::Result;
use crate::request::request_group_man::RequestGroupMan;
pub struct AutoSaveSession {
inner: SaveSessionCommand,
interval: Duration,
last_saved: Instant,
dirty: AtomicBool,
status: CommandStatus,
}
impl AutoSaveSession {
pub fn new(path: PathBuf, interval: Duration, man: Arc<RwLock<RequestGroupMan>>) -> Self {
AutoSaveSession {
inner: SaveSessionCommand::new(path, man),
interval,
last_saved: Instant::now(),
dirty: AtomicBool::new(false),
status: CommandStatus::Pending,
}
}
pub fn mark_dirty(&self) {
self.dirty.store(true, Ordering::SeqCst);
debug!("AutoSaveSession marked as dirty");
}
pub fn is_dirty(&self) -> bool {
self.dirty.load(Ordering::SeqCst)
}
pub fn interval(&self) -> Duration {
self.interval
}
}
#[async_trait]
impl Command for AutoSaveSession {
async fn execute(&mut self) -> Result<()> {
let elapsed = self.last_saved.elapsed();
if elapsed < self.interval {
debug!(
"AutoSave skipped: interval not reached ({:.1}s < {:.1}s)",
elapsed.as_secs_f64(),
self.interval.as_secs_f64()
);
return Ok(());
}
if !self.is_dirty() {
debug!("AutoSave skipped: no changes (dirty=false)");
return Ok(());
}
debug!(
"AutoSave triggered: interval={:.1}s, dirty=true",
elapsed.as_secs_f64()
);
self.inner.execute().await?;
self.last_saved = Instant::now();
self.dirty.store(false, Ordering::SeqCst);
self.status = self.inner.status();
Ok(())
}
fn status(&self) -> CommandStatus {
self.status.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::request::request_group::DownloadOptions;
#[tokio::test]
async fn test_auto_save_creation() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
let auto = AutoSaveSession::new(
PathBuf::from("/tmp/auto.sess"),
Duration::from_secs(10),
man,
);
assert_eq!(auto.status(), CommandStatus::Pending);
assert_eq!(auto.interval(), Duration::from_secs(10));
assert!(!auto.is_dirty());
}
#[tokio::test]
async fn test_auto_save_dirty_flag() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
let auto = AutoSaveSession::new(
PathBuf::from("/tmp/auto.sess"),
Duration::from_secs(10),
man,
);
assert!(!auto.is_dirty());
auto.mark_dirty();
assert!(auto.is_dirty());
}
#[tokio::test]
async fn test_auto_save_skip_when_not_dirty() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
let dir = std::env::temp_dir();
let path = dir.join(format!("test_autosave_clean_{}.sess", std::process::id()));
let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
auto.execute().await.unwrap();
assert!(!path.exists(), "非 dirty 不应写入文件");
let _ = tokio::fs::remove_file(&path).await;
}
#[tokio::test]
async fn test_auto_save_skip_when_interval_not_reached() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
let dir = std::env::temp_dir();
let path = dir.join(format!(
"test_autosave_interval_{}.sess",
std::process::id()
));
let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(999999), man);
auto.mark_dirty();
auto.execute().await.unwrap();
assert!(!path.exists(), "间隔未到不应写入文件");
let _ = tokio::fs::remove_file(&path).await;
}
#[tokio::test]
async fn test_auto_save_triggers_on_both_conditions() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
man.write()
.await
.add_group(
vec!["http://example.com/auto.bin".into()],
DownloadOptions {
split: Some(2),
..Default::default()
},
)
.await
.unwrap();
let dir = std::env::temp_dir();
let path = dir.join(format!("test_autosave_trigger_{}.sess", std::process::id()));
let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
auto.mark_dirty();
auto.execute().await.unwrap();
assert!(path.exists(), "满足间隔+dirty 条件应写入文件");
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert!(content.contains("http://example.com/auto.bin"));
let _ = tokio::fs::remove_file(&path).await;
}
#[tokio::test]
async fn test_auto_save_resets_dirty_after_save() {
let man = Arc::new(RwLock::new(RequestGroupMan::new()));
let dir = std::env::temp_dir();
let path = dir.join(format!("test_autosave_reset_{}.sess", std::process::id()));
let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
auto.mark_dirty();
assert!(auto.is_dirty());
auto.execute().await.unwrap();
assert!(!auto.is_dirty(), "保存后应重置 dirty 标记");
let _ = tokio::fs::remove_file(&path).await;
}
}