Skip to main content

aria2_core/session/
auto_save_session.rs

1use 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}