adana_db/
database.rs

1use std::{
2    fmt::Display,
3    path::Path,
4    sync::{Arc, Mutex},
5};
6
7use crate::FileDbConfig;
8use anyhow::Context;
9use log::debug;
10use serde::de::DeserializeOwned;
11
12const ADANA_DB_DIR: &str = "adana/db";
13
14use super::{
15    FileDb, FileLock, FileLockError, InMemoryDb, Key, Value, file_lock,
16};
17
18fn get_default_db_path() -> Option<Box<Path>> {
19    let mut db_dir = dirs::data_dir().or_else(dirs::home_dir)?;
20    db_dir.push(ADANA_DB_DIR);
21    debug!("db dir: {}", db_dir.as_path().to_string_lossy());
22    if !db_dir.exists() {
23        std::fs::create_dir_all(&db_dir).ok()?;
24    }
25    db_dir.push("adana.db");
26
27    Some(db_dir.into_boxed_path())
28}
29
30#[derive(Debug)]
31pub struct Config {
32    path: Option<Box<Path>>,
33    in_memory: bool,
34    fall_back_in_memory: bool,
35}
36impl Config {
37    pub fn new<P: AsRef<Path>>(
38        path: Option<P>,
39        in_memory: bool,
40        fall_back_in_memory: bool,
41    ) -> Config {
42        if in_memory {
43            Config { in_memory, path: None, fall_back_in_memory: false }
44        } else {
45            Config {
46                in_memory,
47                path: path
48                    .map(|p| {
49                        let path: Box<Path> = p.as_ref().into();
50                        path
51                    })
52                    .or_else(get_default_db_path),
53                fall_back_in_memory,
54            }
55        }
56    }
57}
58
59impl Default for Config {
60    fn default() -> Self {
61        Self {
62            path: get_default_db_path(),
63            in_memory: false,
64            fall_back_in_memory: true,
65        }
66    }
67}
68
69pub enum Db<K: Key, V: Value> {
70    FileBased(FileDb<K, V>),
71    InMemory(InMemoryDb<K, V>),
72}
73
74impl<K, V> Db<K, V>
75where
76    K: 'static + Key + DeserializeOwned + std::fmt::Debug,
77    V: 'static + Value + DeserializeOwned + std::fmt::Debug,
78{
79    fn in_memory_fallback(e: impl Display) -> anyhow::Result<Db<K, V>> {
80        eprintln!("Warning! {e} \nAttempt to open a temporary db...\n",);
81        Ok(Db::InMemory(Default::default()))
82    }
83    pub fn open(config: Config) -> anyhow::Result<Db<K, V>> {
84        if config.in_memory {
85            return Ok(Db::InMemory(Default::default()));
86        }
87        let path = config.path.context("not in memory but path empty")?;
88
89        let file_lock = FileLock::open(path.as_ref());
90        match file_lock {
91            Err(e) if !config.fall_back_in_memory => {
92                Err(anyhow::Error::msg(e.to_string()))
93            }
94            Err(pid_exist @ FileLockError::PidExist(_)) => {
95                eprintln!(
96                    "Warning! {pid_exist} \nAttempt to open a temporary db...\n",
97                );
98                let p = path.as_ref();
99                let pb = p.to_path_buf();
100                match file_lock::read_file(&pb) {
101                    Ok(reader) => {
102                        match bincode::deserialize_from::<_, InMemoryDb<K, V>>(
103                            reader,
104                        ) {
105                            Ok(inner_db) => Ok(Db::InMemory(inner_db)),
106                            Err(e) => {
107                                eprintln!(
108                                    "Warning! {e:?} \nAttempt to deserialize db, could be because it is the first time you use it\n",
109                                );
110                                Self::in_memory_fallback(e)
111                            }
112                        }
113                    }
114                    Err(e) if config.fall_back_in_memory => {
115                        Self::in_memory_fallback(e)
116                    }
117                    Err(e) => Err(e),
118                }
119            }
120            Err(e) => Self::in_memory_fallback(e),
121            Ok(file_lock) => {
122                let inner = match file_lock.read() {
123                    Ok(reader) => {
124                        match bincode::deserialize_from::<_, InMemoryDb<K, V>>(
125                            reader,
126                        ) {
127                            Ok(inner_db) => Arc::new(Mutex::new(inner_db)),
128                            Err(e) => {
129                                eprintln!(
130                                    "Warning! {e:?} \nAttempt to deserialize db, could be because it is the first time you use it\n",
131                                );
132                                Arc::new(Mutex::new(Default::default()))
133                            }
134                        }
135                    }
136                    Err(e) if config.fall_back_in_memory => {
137                        return Self::in_memory_fallback(e);
138                    }
139                    Err(e) => return Err(e),
140                };
141
142                let db_config =
143                    FileDbConfig { file_lock: Arc::new(file_lock), inner };
144                match FileDb::try_from(db_config) {
145                    Ok(file_db) => Ok(Db::FileBased(file_db)),
146                    Err(e) if config.fall_back_in_memory => {
147                        Self::in_memory_fallback(e)
148                    }
149                    Err(e) => Err(e),
150                }
151            }
152        }
153    }
154}
155
156#[cfg(test)]
157mod test {
158
159    use std::fs::File;
160
161    use crate::{Config, Db, DbOp, Op};
162
163    #[test]
164    fn test_file_db_lock() {
165        let _ = File::create("/tmp/adana.db"); // reset the file
166
167        let file_db: Db<u64, String> =
168            Db::open(Config::new(Some("/tmp/adana.db"), false, false)).unwrap();
169
170        let mut file_db = match file_db {
171            Db::FileBased(file_db) => file_db,
172            _ => {
173                panic!("error, should be file db")
174            }
175        };
176        file_db.open_tree("rust");
177
178        for i in 1..100u64 {
179            file_db.insert(i, format!("ok mani{i}"));
180            file_db.insert(i * 100, format!("ok rebenga{i}"));
181        }
182        assert_eq!(Some(198), file_db.len());
183
184        drop(file_db); // force destroying the object to flush db
185
186        let file_db: Db<u64, String> =
187            Db::open(Config::new(Some("/tmp/adana.db"), false, false)).unwrap();
188
189        let mut file_db = match file_db {
190            Db::FileBased(file_db) => file_db,
191            _ => {
192                panic!("error, should be file db")
193            }
194        };
195
196        file_db.open_tree("rust");
197
198        file_db.insert(39912u64, "new!".to_string());
199
200        assert_eq!(Some(199), file_db.len());
201    }
202}