fast_down_gui/persist/
mod.rs1mod 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}