1use std::path::PathBuf;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use tokio::sync::RwLock;
7
8use super::session_serializer::{self, SessionEntry};
9use crate::request::request_group::RequestGroup;
10
11pub struct ActiveSessionManager {
13 pub session_path: PathBuf,
15 pub auto_save_interval: Duration,
17 dirty_flag: AtomicBool,
19}
20
21impl ActiveSessionManager {
22 pub fn new(session_path: PathBuf, auto_save_interval: Duration) -> Self {
36 tracing::info!(
37 "Creating ActiveSessionManager: path={}, interval={:?}",
38 session_path.display(),
39 auto_save_interval
40 );
41
42 ActiveSessionManager {
43 session_path,
44 auto_save_interval,
45 dirty_flag: AtomicBool::new(false),
46 }
47 }
48
49 pub async fn load_session(&self) -> Result<Vec<SessionEntry>, String> {
57 if !self.session_path.exists() {
58 tracing::debug!(
59 "Session file not found, returning empty list: {}",
60 self.session_path.display()
61 );
62 return Ok(vec![]);
63 }
64
65 match session_serializer::load_from_file(&self.session_path).await {
66 Ok(entries) => {
67 tracing::info!(
68 "Session file loaded successfully: {}, entries: {}",
69 self.session_path.display(),
70 entries.len()
71 );
72 Ok(entries)
73 }
74 Err(e) => {
75 let err_msg = format!("Failed to load session file: {}", e);
76 tracing::error!("{}", err_msg);
77 Err(err_msg)
78 }
79 }
80 }
81
82 pub async fn save_session(
94 &self,
95 groups: &[Arc<RwLock<RequestGroup>>],
96 ) -> Result<usize, String> {
97 let mut entries = Vec::new();
99 for group_lock in groups {
100 let group = group_lock.read().await;
101 if let Some(entry) = session_serializer::group_to_entry(&group).await {
102 entries.push(entry);
103 }
104 }
105
106 if entries.is_empty() {
107 tracing::debug!("No active entries to save");
108 return Ok(0);
109 }
110
111 match session_serializer::save_to_file_with_entries(&self.session_path, &entries).await {
113 Ok(_) => {
114 tracing::info!(
115 "Session file saved successfully: {}, entries: {}",
116 self.session_path.display(),
117 entries.len()
118 );
119 Ok(entries.len())
120 }
121 Err(e) => {
122 let err_msg = format!("Failed to save session file: {}", e);
123 tracing::error!("{}", err_msg);
124 Err(err_msg)
125 }
126 }
127 }
128
129 pub fn mark_dirty(&self) {
131 self.dirty_flag.store(true, Ordering::Relaxed);
132 tracing::debug!("Marking session as dirty");
133 }
134
135 pub fn is_dirty(&self) -> bool {
137 self.dirty_flag.load(Ordering::Relaxed)
138 }
139
140 pub fn start_auto_save(self: &Arc<Self>, groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>>) {
154 let mgr = Arc::clone(self);
155
156 tracing::info!(
157 "Starting auto-save task, interval: {:?}",
158 mgr.auto_save_interval
159 );
160
161 tokio::spawn(async move {
162 let mut interval = tokio::time::interval(mgr.auto_save_interval);
163
164 loop {
165 interval.tick().await;
166
167 if !mgr.is_dirty() {
169 tracing::debug!("Auto-save check: no changes, skipping");
170 continue;
171 }
172
173 tracing::debug!("Auto-save check: changes detected, starting save");
174
175 let groups_read = groups.read().await;
177 match mgr.save_session(&groups_read).await {
178 Ok(n) => {
179 tracing::debug!("Auto-save succeeded: saved {} entries", n);
180 mgr.dirty_flag.store(false, Ordering::Relaxed);
182 }
183 Err(e) => {
184 tracing::warn!("Auto-save failed: {} (keeping dirty flag for retry)", e);
185 }
187 }
188 }
189 });
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::request::request_group::{DownloadOptions, GroupId};
197 use tempfile::TempDir;
198
199 #[test]
201 fn test_new_manager() {
202 let temp_dir = TempDir::new().expect("创建临时目录失败");
203 let session_path = temp_dir.path().join("session.txt");
204 let interval = Duration::from_secs(60);
205
206 let manager = ActiveSessionManager::new(session_path.clone(), interval);
207
208 assert_eq!(manager.session_path, session_path, "路径应正确设置");
209 assert_eq!(manager.auto_save_interval, interval, "间隔应正确设置");
210 assert!(!manager.is_dirty(), "新创建的管理器不应是脏状态");
211 }
212
213 #[tokio::test]
215 async fn test_load_nonexistent_file_returns_empty() {
216 let temp_dir = TempDir::new().expect("创建临时目录失败");
217 let nonexistent_path = temp_dir.path().join("nonexistent_session.txt");
218
219 let manager = ActiveSessionManager::new(nonexistent_path, Duration::from_secs(60));
220 let result = manager.load_session().await;
221
222 assert!(result.is_ok(), "文件不存在不应返回错误");
223 let entries = result.unwrap();
224 assert!(entries.is_empty(), "文件不存在时应返回空列表");
225 }
226
227 #[tokio::test]
229 async fn test_load_save_roundtrip() {
230 let temp_dir = TempDir::new().expect("创建临时目录失败");
231 let session_path = temp_dir.path().join("roundtrip_session.txt");
232
233 let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
234
235 let gid1 = GroupId::new(0xd270c8a2);
237 let options1 = DownloadOptions {
238 dir: Some("/downloads".to_string()),
239 split: Some(4),
240 ..Default::default()
241 };
242 let group1 = Arc::new(RwLock::new(RequestGroup::new(
243 gid1,
244 vec!["http://example.com/file1.zip".to_string()],
245 options1,
246 )));
247
248 let gid2 = GroupId::new(0xabcdef01);
249 let group2 = Arc::new(RwLock::new(RequestGroup::new(
250 gid2,
251 vec![
252 "http://mirror.com/file2.iso".to_string(),
253 "ftp://backup.com/file2.iso".to_string(),
254 ],
255 DownloadOptions::default(),
256 )));
257
258 let groups = vec![group1, group2];
259
260 let save_result = manager.save_session(&groups).await;
262 assert!(save_result.is_ok(), "保存应成功");
263 let saved_count = save_result.unwrap();
264 assert!(saved_count > 0, "应保存至少 1 个条目");
265
266 let load_result = manager.load_session().await;
268 assert!(load_result.is_ok(), "加载应成功");
269 let entries = load_result.unwrap();
270
271 assert_eq!(entries.len(), saved_count, "加载的条目数应与保存的一致");
272
273 assert!(
275 entries
276 .iter()
277 .any(|e| e.uris.contains(&"http://example.com/file1.zip".to_string())),
278 "应包含第一个 URI"
279 );
280 assert!(
281 entries
282 .iter()
283 .any(|e| e.uris.contains(&"http://mirror.com/file2.iso".to_string())),
284 "应包含第二个 URI"
285 );
286 }
287
288 #[test]
290 fn test_mark_dirty_and_check() {
291 let temp_dir = TempDir::new().expect("创建临时目录失败");
292 let session_path = temp_dir.path().join("dirty_test.txt");
293
294 let manager = ActiveSessionManager::new(session_path, Duration::from_secs(30));
295
296 assert!(!manager.is_dirty(), "初始状态应是干净的");
298
299 manager.mark_dirty();
301 assert!(manager.is_dirty(), "mark_dirty 后应为脏状态");
302
303 manager.mark_dirty();
305 assert!(manager.is_dirty(), "重复 mark_dirty 应保持脏状态");
306 }
307
308 #[tokio::test]
312 async fn test_auto_save_skips_when_clean() {
313 let temp_dir = TempDir::new().expect("创建临时目录失败");
314 let session_path = temp_dir.path().join("auto_skip_test.txt");
315
316 let manager = Arc::new(ActiveSessionManager::new(
317 session_path.clone(),
318 Duration::from_millis(50), ));
320
321 let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> = Arc::new(RwLock::new(Vec::new()));
322
323 manager.start_auto_save(Arc::clone(&groups));
325
326 tokio::time::sleep(Duration::from_millis(200)).await;
328
329 assert!(!session_path.exists(), "dirty=false 时不应创建会话文件");
331 }
332
333 #[tokio::test]
335 async fn test_save_creates_file() {
336 let temp_dir = TempDir::new().expect("创建临时目录失败");
337 let session_path = temp_dir.path().join("file_creation_test.txt");
338
339 let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
340
341 assert!(!session_path.exists(), "保存前文件不应存在");
343
344 let gid = GroupId::new(12345);
346 let group = Arc::new(RwLock::new(RequestGroup::new(
347 gid,
348 vec!["http://test.com/file.bin".to_string()],
349 DownloadOptions::default(),
350 )));
351
352 let result = manager.save_session(&[group]).await;
354 assert!(result.is_ok(), "保存应成功");
355
356 assert!(session_path.exists(), "保存后文件应存在于指定路径");
358
359 let content = tokio::fs::read_to_string(&session_path)
361 .await
362 .expect("读取文件失败");
363 assert!(!content.is_empty(), "文件内容不应为空");
364 assert!(
365 content.contains("http://test.com/file.bin"),
366 "文件应包含保存的 URI"
367 );
368 }
369
370 #[tokio::test]
372 async fn test_multiple_saves_overwrite() {
373 let temp_dir = TempDir::new().expect("创建临时目录失败");
374 let session_path = temp_dir.path().join("overwrite_test.txt");
375
376 let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
377
378 let gid1 = GroupId::new(1);
380 let group1 = Arc::new(RwLock::new(RequestGroup::new(
381 gid1,
382 vec!["http://first.com/a.txt".to_string()],
383 DownloadOptions::default(),
384 )));
385 let result1 = manager.save_session(&[group1]).await;
386 assert!(result1.is_ok());
387
388 let gid2 = GroupId::new(2);
390 let group2 = Arc::new(RwLock::new(RequestGroup::new(
391 gid2,
392 vec!["http://second.com/b.txt".to_string()],
393 DownloadOptions::default(),
394 )));
395 let result2 = manager.save_session(&[group2]).await;
396 assert!(result2.is_ok());
397
398 let entries = manager.load_session().await.expect("加载失败");
400 assert_eq!(entries.len(), 1, "应只有 1 个条目(最新的)");
401 assert!(
402 entries[0]
403 .uris
404 .contains(&"http://second.com/b.txt".to_string()),
405 "应包含最新保存的 URI"
406 );
407 assert!(
408 !entries[0]
409 .uris
410 .contains(&"http://first.com/a.txt".to_string()),
411 "不应包含旧的 URI"
412 );
413 }
414
415 #[tokio::test]
417 async fn test_save_empty_groups() {
418 let temp_dir = TempDir::new().expect("创建临时目录失败");
419 let session_path = temp_dir.path().join("empty_groups_test.txt");
420
421 let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
422
423 let empty_groups: Vec<Arc<RwLock<RequestGroup>>> = vec![];
425 let result = manager.save_session(&empty_groups).await;
426
427 assert!(result.is_ok(), "保存空列表应成功");
428 assert_eq!(result.unwrap(), 0, "应返回 0 个条目");
429
430 if session_path.exists() {
432 let content = tokio::fs::read_to_string(&session_path)
433 .await
434 .expect("读取文件失败");
435 assert!(content.is_empty(), "空组列表应产生空文件");
436 }
437 }
438
439 #[tokio::test]
441 async fn test_auto_save_triggers_on_dirty() {
442 let temp_dir = TempDir::new().expect("创建临时目录失败");
443 let session_path = temp_dir.path().join("auto_trigger_test.txt");
444
445 let manager = Arc::new(ActiveSessionManager::new(
446 session_path.clone(),
447 Duration::from_millis(50), ));
449
450 let gid = GroupId::new(99999);
452 let group = Arc::new(RwLock::new(RequestGroup::new(
453 gid,
454 vec!["http://auto-save-test.com/data.bin".to_string()],
455 DownloadOptions::default(),
456 )));
457
458 let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> =
459 Arc::new(RwLock::new(vec![group]));
460
461 manager.start_auto_save(Arc::clone(&groups));
463
464 manager.mark_dirty();
466
467 tokio::time::sleep(Duration::from_millis(300)).await;
469
470 if session_path.exists() {
474 let content = tokio::fs::read_to_string(&session_path)
475 .await
476 .expect("读取文件失败");
477 assert!(
478 content.contains("http://auto-save-test.com/data.bin") || content.is_empty(),
479 "文件应包含保存的数据或为空(取决于时序)"
480 );
481 }
482 }
484
485 #[test]
487 fn test_different_intervals() {
488 let temp_dir = TempDir::new().expect("创建临时目录失败");
489
490 let intervals = [
492 Duration::from_secs(1),
493 Duration::from_secs(30),
494 Duration::from_secs(60),
495 Duration::from_secs(300),
496 Duration::from_millis(500),
497 ];
498
499 for (i, interval) in intervals.iter().enumerate() {
500 let path = temp_dir.path().join(format!("interval_test_{}.txt", i));
501 let manager = ActiveSessionManager::new(path, *interval);
502 assert_eq!(
503 manager.auto_save_interval, *interval,
504 "间隔 {} 应正确设置",
505 i
506 );
507 }
508 }
509}