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,
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}