aria2_core/session/
auto_save_session.rs1use std::path::PathBuf;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::{Duration, Instant};
5use tokio::sync::RwLock;
6
7use async_trait::async_trait;
8use tracing::debug;
9
10use super::save_session_command::SaveSessionCommand;
11use crate::engine::command::{Command, CommandStatus};
12use crate::error::Result;
13use crate::request::request_group_man::RequestGroupMan;
14
15pub struct AutoSaveSession {
16 inner: SaveSessionCommand,
17 interval: Duration,
18 last_saved: Instant,
19 dirty: AtomicBool,
20 status: CommandStatus,
21}
22
23impl AutoSaveSession {
24 pub fn new(path: PathBuf, interval: Duration, man: Arc<RwLock<RequestGroupMan>>) -> Self {
25 AutoSaveSession {
26 inner: SaveSessionCommand::new(path, man),
27 interval,
28 last_saved: Instant::now(),
29 dirty: AtomicBool::new(false),
30 status: CommandStatus::Pending,
31 }
32 }
33
34 pub fn mark_dirty(&self) {
35 self.dirty.store(true, Ordering::SeqCst);
36 debug!("AutoSaveSession marked as dirty");
37 }
38
39 pub fn is_dirty(&self) -> bool {
40 self.dirty.load(Ordering::SeqCst)
41 }
42
43 pub fn interval(&self) -> Duration {
44 self.interval
45 }
46}
47
48#[async_trait]
49impl Command for AutoSaveSession {
50 async fn execute(&mut self) -> Result<()> {
51 let elapsed = self.last_saved.elapsed();
52
53 if elapsed < self.interval {
54 debug!(
55 "AutoSave skipped: interval not reached ({:.1}s < {:.1}s)",
56 elapsed.as_secs_f64(),
57 self.interval.as_secs_f64()
58 );
59 return Ok(());
60 }
61
62 if !self.is_dirty() {
63 debug!("AutoSave skipped: no changes (dirty=false)");
64 return Ok(());
65 }
66
67 debug!(
68 "AutoSave triggered: interval={:.1}s, dirty=true",
69 elapsed.as_secs_f64()
70 );
71
72 self.inner.execute().await?;
73 self.last_saved = Instant::now();
74 self.dirty.store(false, Ordering::SeqCst);
75 self.status = self.inner.status();
76 Ok(())
77 }
78
79 fn status(&self) -> CommandStatus {
80 self.status.clone()
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::request::request_group::DownloadOptions;
88
89 #[tokio::test]
90 async fn test_auto_save_creation() {
91 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
92 let auto = AutoSaveSession::new(
93 PathBuf::from("/tmp/auto.sess"),
94 Duration::from_secs(10),
95 man,
96 );
97 assert_eq!(auto.status(), CommandStatus::Pending);
98 assert_eq!(auto.interval(), Duration::from_secs(10));
99 assert!(!auto.is_dirty());
100 }
101
102 #[tokio::test]
103 async fn test_auto_save_dirty_flag() {
104 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
105 let auto = AutoSaveSession::new(
106 PathBuf::from("/tmp/auto.sess"),
107 Duration::from_secs(10),
108 man,
109 );
110
111 assert!(!auto.is_dirty());
112 auto.mark_dirty();
113 assert!(auto.is_dirty());
114 }
115
116 #[tokio::test]
117 async fn test_auto_save_skip_when_not_dirty() {
118 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
119 let dir = std::env::temp_dir();
120 let path = dir.join(format!("test_autosave_clean_{}.sess", std::process::id()));
121
122 let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
123
124 auto.execute().await.unwrap();
125 assert!(!path.exists(), "非 dirty 不应写入文件");
126
127 let _ = tokio::fs::remove_file(&path).await;
128 }
129
130 #[tokio::test]
131 async fn test_auto_save_skip_when_interval_not_reached() {
132 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
133 let dir = std::env::temp_dir();
134 let path = dir.join(format!(
135 "test_autosave_interval_{}.sess",
136 std::process::id()
137 ));
138
139 let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(999999), man);
140 auto.mark_dirty();
141
142 auto.execute().await.unwrap();
143 assert!(!path.exists(), "间隔未到不应写入文件");
144
145 let _ = tokio::fs::remove_file(&path).await;
146 }
147
148 #[tokio::test]
149 async fn test_auto_save_triggers_on_both_conditions() {
150 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
151 man.write()
152 .await
153 .add_group(
154 vec!["http://example.com/auto.bin".into()],
155 DownloadOptions {
156 split: Some(2),
157 ..Default::default()
158 },
159 )
160 .await
161 .unwrap();
162
163 let dir = std::env::temp_dir();
164 let path = dir.join(format!("test_autosave_trigger_{}.sess", std::process::id()));
165
166 let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
167 auto.mark_dirty();
168
169 auto.execute().await.unwrap();
170 assert!(path.exists(), "满足间隔+dirty 条件应写入文件");
171
172 let content = tokio::fs::read_to_string(&path).await.unwrap();
173 assert!(content.contains("http://example.com/auto.bin"));
174
175 let _ = tokio::fs::remove_file(&path).await;
176 }
177
178 #[tokio::test]
179 async fn test_auto_save_resets_dirty_after_save() {
180 let man = Arc::new(RwLock::new(RequestGroupMan::new()));
181 let dir = std::env::temp_dir();
182 let path = dir.join(format!("test_autosave_reset_{}.sess", std::process::id()));
183
184 let mut auto = AutoSaveSession::new(path.clone(), Duration::from_secs(0), man);
185 auto.mark_dirty();
186 assert!(auto.is_dirty());
187
188 auto.execute().await.unwrap();
189 assert!(!auto.is_dirty(), "保存后应重置 dirty 标记");
190
191 let _ = tokio::fs::remove_file(&path).await;
192 }
193}