Skip to main content

fast_down_gui/persist/
mod.rs

1mod config;
2mod entry;
3mod loader;
4
5pub use config::*;
6pub use entry::*;
7
8use crate::persist::loader::{BoxLoader, Loader};
9use color_eyre::Result;
10use dashmap::DashMap;
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13use std::{
14    io::Write,
15    ops::Range,
16    path::PathBuf,
17    sync::{
18        Arc,
19        atomic::{AtomicBool, AtomicI32, Ordering},
20    },
21    time::Duration,
22};
23use tokio::{fs, task::JoinHandle};
24use tracing::{error, info};
25
26pub const DB_NAME: &str = "fd-state-gui.fdb";
27lazy_static::lazy_static! {
28    pub static ref DB_DIR: PathBuf = {
29        let db_dir = dirs::data_dir()
30            .and_then(|p| soft_canonicalize::soft_canonicalize(p).ok())
31            .map(|p| p.join("fast-down-gui"))
32            .unwrap_or_default();
33        let _ = std::fs::create_dir_all(&db_dir);
34        db_dir
35    };
36    pub static ref DB_PATH: PathBuf = DB_DIR.join(DB_NAME);
37}
38
39#[derive(Serialize, Deserialize, Debug, Default)]
40pub struct DatabaseInner {
41    pub data: DashMap<i32, DatabaseEntry>,
42    pub download_config: Mutex<DownloadConfig>,
43    pub general_config: Mutex<GeneralConfig>,
44    pub max_gid: AtomicI32,
45}
46
47impl DatabaseInner {
48    pub fn flush(&self) -> color_eyre::Result<()> {
49        let content = bitcode::serialize(self)?;
50        let tmp_path = DB_PATH.with_added_extension("tmp");
51        let mut file = std::fs::OpenOptions::new()
52            .truncate(true)
53            .create(true)
54            .write(true)
55            .open(&tmp_path)?;
56        file.write_all(&content)?;
57        file.sync_all()?;
58        std::fs::rename(tmp_path, &*DB_PATH)?;
59        Ok(())
60    }
61
62    pub fn next_gid(&self) -> i32 {
63        self.max_gid.fetch_add(1, Ordering::SeqCst)
64    }
65
66    pub fn set_auto_start(&self, value: bool) {
67        self.general_config.lock().auto_start = value;
68    }
69
70    pub fn is_auto_start(&self) -> bool {
71        self.general_config.lock().auto_start
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct Database {
77    pub inner: Arc<DatabaseInner>,
78    pub is_dirty: Arc<AtomicBool>,
79    pub handle: Arc<JoinHandle<()>>,
80}
81
82impl Database {
83    pub async fn new() -> Self {
84        if !fs::try_exists(&*DB_PATH).await.unwrap_or(false) {
85            let _ = fs::rename(DB_DIR.join("fd-state-v1-gui.fdb"), &*DB_PATH).await;
86        }
87        let inner = fs::read(&*DB_PATH)
88            .await
89            .ok()
90            .and_then(|bytes| BoxLoader.load(&bytes));
91        if inner.is_none() {
92            let _ = tokio::fs::rename(&*DB_PATH, DB_PATH.with_added_extension("bak")).await;
93        }
94        let inner: Arc<_> = inner.unwrap_or_default().into();
95        let is_dirty = Arc::new(AtomicBool::new(false));
96        let handle = tokio::spawn({
97            let inner = inner.clone();
98            let is_dirty = is_dirty.clone();
99            async move {
100                info!("后台保存线程启动");
101                loop {
102                    tokio::time::sleep(Duration::from_secs(5)).await;
103                    if is_dirty.swap(false, Ordering::Relaxed) {
104                        info!("数据库自动保存中……");
105                        let inner = inner.clone();
106                        let res = tokio::task::spawn_blocking(move || inner.flush()).await;
107                        match res {
108                            Ok(Ok(())) => info!("数据库保存成功"),
109                            Ok(Err(e)) => {
110                                error!(err = ?e, "无法保存到数据库");
111                                is_dirty.store(true, Ordering::Relaxed);
112                            }
113                            Err(e) => {
114                                error!(err = ?e, "无法保存到数据库");
115                                is_dirty.store(true, Ordering::Relaxed);
116                            }
117                        }
118                    }
119                }
120            }
121        });
122        Database {
123            inner,
124            is_dirty,
125            handle: handle.into(),
126        }
127    }
128
129    pub fn get_download_config(&self) -> DownloadConfig {
130        self.inner.download_config.lock().clone()
131    }
132
133    pub fn get_ui_download_config(&self) -> crate::ui::DownloadConfig {
134        self.inner.download_config.lock().to_ui_download_config()
135    }
136
137    pub fn set_download_config(&self, config: impl Into<DownloadConfig>) {
138        *self.inner.download_config.lock() = config.into();
139        self.is_dirty.store(true, Ordering::Relaxed);
140    }
141
142    pub fn get_general_config(&self) -> GeneralConfig {
143        self.inner.general_config.lock().clone()
144    }
145
146    pub fn get_ui_general_config(&self) -> crate::ui::GeneralConfig {
147        self.inner.general_config.lock().to_ui_general_config()
148    }
149
150    pub fn set_general_config(&self, config: impl Into<GeneralConfig>) {
151        *self.inner.general_config.lock() = config.into();
152        self.is_dirty.store(true, Ordering::Relaxed);
153    }
154
155    pub fn next_gid(&self) -> i32 {
156        self.inner.next_gid()
157    }
158
159    pub fn init_entry(&self, gid: i32, entry: DatabaseEntry) -> Result<()> {
160        self.inner.data.insert(gid, entry);
161        self.is_dirty.store(true, Ordering::Relaxed);
162        Ok(())
163    }
164
165    pub fn update_entry(&self, gid: i32, progress: Vec<Range<u64>>, elapsed: Duration) {
166        if let Some(mut e) = self.inner.data.get_mut(&gid) {
167            e.progress = progress;
168            e.elapsed = elapsed;
169            self.is_dirty.store(true, Ordering::Relaxed);
170        }
171    }
172
173    pub fn update_status(&self, gid: i32, status: Status) {
174        if let Some(mut e) = self.inner.data.get_mut(&gid) {
175            e.status = status;
176            self.is_dirty.store(true, Ordering::Relaxed);
177        }
178    }
179
180    pub fn remove_entry(&self, gid: i32) -> Result<()> {
181        self.inner.data.remove(&gid);
182        self.is_dirty.store(true, Ordering::Relaxed);
183        Ok(())
184    }
185
186    pub fn flush_force_sync(&self) -> Result<()> {
187        if self.is_dirty.swap(false, Ordering::Relaxed) {
188            match self.inner.flush() {
189                Ok(()) => info!("数据库保存成功"),
190                Err(e) => {
191                    error!(err = ?e, "无法保存到数据库");
192                    self.is_dirty.store(true, Ordering::Relaxed);
193                    Err(e)?
194                }
195            }
196        }
197        Ok(())
198    }
199
200    pub fn is_auto_start(&self) -> bool {
201        self.inner.is_auto_start()
202    }
203
204    pub fn set_auto_start(&self, value: bool) {
205        self.inner.set_auto_start(value);
206    }
207}
208
209impl Drop for Database {
210    fn drop(&mut self) {
211        self.handle.abort();
212        if self.is_dirty.load(Ordering::Relaxed) {
213            let _ = self.inner.flush();
214        }
215    }
216}