use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::RwLock;
use super::session_serializer::{self, SessionEntry};
use crate::request::request_group::RequestGroup;
pub struct ActiveSessionManager {
pub session_path: PathBuf,
pub auto_save_interval: Duration,
dirty_flag: AtomicBool,
}
impl ActiveSessionManager {
pub fn new(session_path: PathBuf, auto_save_interval: Duration) -> Self {
tracing::info!(
"Creating ActiveSessionManager: path={}, interval={:?}",
session_path.display(),
auto_save_interval
);
ActiveSessionManager {
session_path,
auto_save_interval,
dirty_flag: AtomicBool::new(false),
}
}
pub async fn load_session(&self) -> Result<Vec<SessionEntry>, String> {
if !self.session_path.exists() {
tracing::debug!(
"Session file not found, returning empty list: {}",
self.session_path.display()
);
return Ok(vec![]);
}
match session_serializer::load_from_file(&self.session_path).await {
Ok(entries) => {
tracing::info!(
"Session file loaded successfully: {}, entries: {}",
self.session_path.display(),
entries.len()
);
Ok(entries)
}
Err(e) => {
let err_msg = format!("Failed to load session file: {}", e);
tracing::error!("{}", err_msg);
Err(err_msg)
}
}
}
pub async fn save_session(
&self,
groups: &[Arc<RwLock<RequestGroup>>],
) -> Result<usize, String> {
let mut entries = Vec::new();
for group_lock in groups {
let group = group_lock.read().await;
if let Some(entry) = session_serializer::group_to_entry(&group).await {
entries.push(entry);
}
}
if entries.is_empty() {
tracing::debug!("No active entries to save");
return Ok(0);
}
match session_serializer::save_to_file_with_entries(&self.session_path, &entries).await {
Ok(_) => {
tracing::info!(
"Session file saved successfully: {}, entries: {}",
self.session_path.display(),
entries.len()
);
Ok(entries.len())
}
Err(e) => {
let err_msg = format!("Failed to save session file: {}", e);
tracing::error!("{}", err_msg);
Err(err_msg)
}
}
}
pub fn mark_dirty(&self) {
self.dirty_flag.store(true, Ordering::Relaxed);
tracing::debug!("Marking session as dirty");
}
pub fn is_dirty(&self) -> bool {
self.dirty_flag.load(Ordering::Relaxed)
}
pub fn start_auto_save(self: &Arc<Self>, groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>>) {
let mgr = Arc::clone(self);
tracing::info!(
"Starting auto-save task, interval: {:?}",
mgr.auto_save_interval
);
tokio::spawn(async move {
let mut interval = tokio::time::interval(mgr.auto_save_interval);
loop {
interval.tick().await;
if !mgr.is_dirty() {
tracing::debug!("Auto-save check: no changes, skipping");
continue;
}
tracing::debug!("Auto-save check: changes detected, starting save");
let groups_read = groups.read().await;
match mgr.save_session(&groups_read).await {
Ok(n) => {
tracing::debug!("Auto-save succeeded: saved {} entries", n);
mgr.dirty_flag.store(false, Ordering::Relaxed);
}
Err(e) => {
tracing::warn!("Auto-save failed: {} (keeping dirty flag for retry)", e);
}
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::request::request_group::{DownloadOptions, GroupId};
use tempfile::TempDir;
#[test]
fn test_new_manager() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("session.txt");
let interval = Duration::from_secs(60);
let manager = ActiveSessionManager::new(session_path.clone(), interval);
assert_eq!(manager.session_path, session_path, "路径应正确设置");
assert_eq!(manager.auto_save_interval, interval, "间隔应正确设置");
assert!(!manager.is_dirty(), "新创建的管理器不应是脏状态");
}
#[tokio::test]
async fn test_load_nonexistent_file_returns_empty() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let nonexistent_path = temp_dir.path().join("nonexistent_session.txt");
let manager = ActiveSessionManager::new(nonexistent_path, Duration::from_secs(60));
let result = manager.load_session().await;
assert!(result.is_ok(), "文件不存在不应返回错误");
let entries = result.unwrap();
assert!(entries.is_empty(), "文件不存在时应返回空列表");
}
#[tokio::test]
async fn test_load_save_roundtrip() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("roundtrip_session.txt");
let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
let gid1 = GroupId::new(0xd270c8a2);
let options1 = DownloadOptions {
dir: Some("/downloads".to_string()),
split: Some(4),
..Default::default()
};
let group1 = Arc::new(RwLock::new(RequestGroup::new(
gid1,
vec!["http://example.com/file1.zip".to_string()],
options1,
)));
let gid2 = GroupId::new(0xabcdef01);
let group2 = Arc::new(RwLock::new(RequestGroup::new(
gid2,
vec![
"http://mirror.com/file2.iso".to_string(),
"ftp://backup.com/file2.iso".to_string(),
],
DownloadOptions::default(),
)));
let groups = vec![group1, group2];
let save_result = manager.save_session(&groups).await;
assert!(save_result.is_ok(), "保存应成功");
let saved_count = save_result.unwrap();
assert!(saved_count > 0, "应保存至少 1 个条目");
let load_result = manager.load_session().await;
assert!(load_result.is_ok(), "加载应成功");
let entries = load_result.unwrap();
assert_eq!(entries.len(), saved_count, "加载的条目数应与保存的一致");
assert!(
entries
.iter()
.any(|e| e.uris.contains(&"http://example.com/file1.zip".to_string())),
"应包含第一个 URI"
);
assert!(
entries
.iter()
.any(|e| e.uris.contains(&"http://mirror.com/file2.iso".to_string())),
"应包含第二个 URI"
);
}
#[test]
fn test_mark_dirty_and_check() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("dirty_test.txt");
let manager = ActiveSessionManager::new(session_path, Duration::from_secs(30));
assert!(!manager.is_dirty(), "初始状态应是干净的");
manager.mark_dirty();
assert!(manager.is_dirty(), "mark_dirty 后应为脏状态");
manager.mark_dirty();
assert!(manager.is_dirty(), "重复 mark_dirty 应保持脏状态");
}
#[tokio::test]
async fn test_auto_save_skips_when_clean() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("auto_skip_test.txt");
let manager = Arc::new(ActiveSessionManager::new(
session_path.clone(),
Duration::from_millis(50), ));
let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> = Arc::new(RwLock::new(Vec::new()));
manager.start_auto_save(Arc::clone(&groups));
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(!session_path.exists(), "dirty=false 时不应创建会话文件");
}
#[tokio::test]
async fn test_save_creates_file() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("file_creation_test.txt");
let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
assert!(!session_path.exists(), "保存前文件不应存在");
let gid = GroupId::new(12345);
let group = Arc::new(RwLock::new(RequestGroup::new(
gid,
vec!["http://test.com/file.bin".to_string()],
DownloadOptions::default(),
)));
let result = manager.save_session(&[group]).await;
assert!(result.is_ok(), "保存应成功");
assert!(session_path.exists(), "保存后文件应存在于指定路径");
let content = tokio::fs::read_to_string(&session_path)
.await
.expect("读取文件失败");
assert!(!content.is_empty(), "文件内容不应为空");
assert!(
content.contains("http://test.com/file.bin"),
"文件应包含保存的 URI"
);
}
#[tokio::test]
async fn test_multiple_saves_overwrite() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("overwrite_test.txt");
let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
let gid1 = GroupId::new(1);
let group1 = Arc::new(RwLock::new(RequestGroup::new(
gid1,
vec!["http://first.com/a.txt".to_string()],
DownloadOptions::default(),
)));
let result1 = manager.save_session(&[group1]).await;
assert!(result1.is_ok());
let gid2 = GroupId::new(2);
let group2 = Arc::new(RwLock::new(RequestGroup::new(
gid2,
vec!["http://second.com/b.txt".to_string()],
DownloadOptions::default(),
)));
let result2 = manager.save_session(&[group2]).await;
assert!(result2.is_ok());
let entries = manager.load_session().await.expect("加载失败");
assert_eq!(entries.len(), 1, "应只有 1 个条目(最新的)");
assert!(
entries[0]
.uris
.contains(&"http://second.com/b.txt".to_string()),
"应包含最新保存的 URI"
);
assert!(
!entries[0]
.uris
.contains(&"http://first.com/a.txt".to_string()),
"不应包含旧的 URI"
);
}
#[tokio::test]
async fn test_save_empty_groups() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("empty_groups_test.txt");
let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
let empty_groups: Vec<Arc<RwLock<RequestGroup>>> = vec![];
let result = manager.save_session(&empty_groups).await;
assert!(result.is_ok(), "保存空列表应成功");
assert_eq!(result.unwrap(), 0, "应返回 0 个条目");
if session_path.exists() {
let content = tokio::fs::read_to_string(&session_path)
.await
.expect("读取文件失败");
assert!(content.is_empty(), "空组列表应产生空文件");
}
}
#[tokio::test]
async fn test_auto_save_triggers_on_dirty() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let session_path = temp_dir.path().join("auto_trigger_test.txt");
let manager = Arc::new(ActiveSessionManager::new(
session_path.clone(),
Duration::from_millis(50), ));
let gid = GroupId::new(99999);
let group = Arc::new(RwLock::new(RequestGroup::new(
gid,
vec!["http://auto-save-test.com/data.bin".to_string()],
DownloadOptions::default(),
)));
let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> =
Arc::new(RwLock::new(vec![group]));
manager.start_auto_save(Arc::clone(&groups));
manager.mark_dirty();
tokio::time::sleep(Duration::from_millis(300)).await;
if session_path.exists() {
let content = tokio::fs::read_to_string(&session_path)
.await
.expect("读取文件失败");
assert!(
content.contains("http://auto-save-test.com/data.bin") || content.is_empty(),
"文件应包含保存的数据或为空(取决于时序)"
);
}
}
#[test]
fn test_different_intervals() {
let temp_dir = TempDir::new().expect("创建临时目录失败");
let intervals = [
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(60),
Duration::from_secs(300),
Duration::from_millis(500),
];
for (i, interval) in intervals.iter().enumerate() {
let path = temp_dir.path().join(format!("interval_test_{}.txt", i));
let manager = ActiveSessionManager::new(path, *interval);
assert_eq!(
manager.auto_save_interval, *interval,
"间隔 {} 应正确设置",
i
);
}
}
}