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