kvdb_file/
lib.rs

1use kvdb::{DBKeyValue, DBOp, DBTransaction, DBValue, KeyValueDB};
2use kvdb_memorydb::InMemory;
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7/// A key-value database fulfilling the `KeyValueDB` trait, living in file.
8/// This is generally intended for tests and is not particularly optimized.
9#[derive(Default)]
10pub struct InFile {
11    path: String,
12    in_memory: InMemory,
13}
14impl InFile {
15    fn col_path(&self, col: u32) -> PathBuf {
16        let mut path = PathBuf::from(&self.path);
17        path.push(col.to_string());
18        path
19    }
20    fn key2file(&self, col: u32, key: &[u8]) -> PathBuf {
21        let mut path = PathBuf::from(&self.path);
22        path.push(col.to_string());
23        path.push(format!("0x{}", hex::encode(key)));
24        path
25    }
26    fn file2key(path: &Path) -> Option<Vec<u8>> {
27        if let Some(name) = path.file_name() {
28            let name = name.to_string_lossy();
29            if let Ok(key) = hex::decode(&name[2..]) {
30                return Some(key);
31            }
32        }
33        None
34    }
35    pub fn open<P: AsRef<Path>>(path: P, num_cols: u32) -> Result<InFile, io::Error> {
36        let in_memory = kvdb_memorydb::create(num_cols);
37        let mut txn = DBTransaction::new();
38        for col in 0..num_cols {
39            let col_dir = path.as_ref().join(col.to_string());
40            fs::create_dir_all(&col_dir)?;
41            for entry in fs::read_dir(col_dir)? {
42                let file = entry?.path();
43                if file.is_file() {
44                    if let Some(key) = Self::file2key(&file) {
45                        let value = fs::read(file)?;
46                        txn.put_vec(col, &key, value);
47                    }
48                }
49            }
50        }
51
52        in_memory.write(txn)?;
53        Ok(InFile {
54            path: path.as_ref().to_string_lossy().into_owned(),
55            in_memory,
56        })
57    }
58}
59
60impl KeyValueDB for InFile {
61    fn get(&self, col: u32, key: &[u8]) -> io::Result<Option<DBValue>> {
62        self.in_memory.get(col, key)
63    }
64
65    fn get_by_prefix(&self, col: u32, prefix: &[u8]) -> io::Result<Option<Vec<u8>>> {
66        self.in_memory.get_by_prefix(col, prefix)
67    }
68
69    fn write(&self, transaction: DBTransaction) -> io::Result<()> {
70        for op in &transaction.ops {
71            match op {
72                DBOp::Insert { col, key, value } => {
73                    let file = self.key2file(*col, key);
74                    fs::write(file, value)?;
75                }
76                DBOp::Delete { col, key } => {
77                    let file = self.key2file(*col, key);
78                    if file.is_file() {
79                        fs::remove_file(file)?;
80                    }
81                }
82                DBOp::DeletePrefix { col, prefix } => {
83                    let col_dir = self.col_path(*col);
84                    if prefix.is_empty() {
85                        for entry in fs::read_dir(col_dir)? {
86                            let file = entry?.path();
87                            if file.is_file() {
88                                fs::remove_file(file)?;
89                            }
90                        }
91                    } else {
92                        for entry in fs::read_dir(col_dir)? {
93                            let file = entry?.path();
94                            if file.is_file() {
95                                if let Some(key) = Self::file2key(&file) {
96                                    if key.starts_with(&prefix) {
97                                        fs::remove_file(file)?;
98                                    }
99                                }
100                            }
101                        }
102                    }
103                }
104            }
105        }
106        self.in_memory.write(transaction)
107    }
108
109    // NOTE: clones the whole db
110    fn iter<'a>(&'a self, col: u32) -> Box<dyn Iterator<Item = io::Result<DBKeyValue>> + 'a> {
111        self.in_memory.iter(col)
112    }
113
114    // NOTE: clones the whole db
115    fn iter_with_prefix<'a>(
116        &'a self,
117        col: u32,
118        prefix: &'a [u8],
119    ) -> Box<dyn Iterator<Item = io::Result<DBKeyValue>> + 'a> {
120        self.in_memory.iter_with_prefix(col, prefix)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::InFile;
127    use kvdb_shared_tests as st;
128    use std::time::SystemTime;
129    use std::{fs, io};
130
131    fn timestramp() -> u64 {
132        let dur = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH);
133        dur.unwrap().as_nanos() as u64
134    }
135    #[test]
136    fn get_fails_with_non_existing_column() -> io::Result<()> {
137        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
138        st::test_get_fails_with_non_existing_column(&db)?;
139        fs::remove_dir_all(&db.path)
140    }
141
142    #[test]
143    fn put_and_get() -> io::Result<()> {
144        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
145        st::test_put_and_get(&db)?;
146        fs::remove_dir_all(&db.path)
147    }
148
149    #[test]
150    fn delete_and_get() -> io::Result<()> {
151        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
152        st::test_delete_and_get(&db)?;
153        fs::remove_dir_all(&db.path)
154    }
155
156    #[test]
157    fn delete_prefix() -> io::Result<()> {
158        let db = InFile::open(format!("{:?}", timestramp()), st::DELETE_PREFIX_NUM_COLUMNS)?;
159        st::test_delete_prefix(&db)?;
160        fs::remove_dir_all(&db.path)
161    }
162
163    #[test]
164    fn iter() -> io::Result<()> {
165        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
166        st::test_iter(&db)?;
167        fs::remove_dir_all(&db.path)
168    }
169
170    #[test]
171    fn iter_with_prefix() -> io::Result<()> {
172        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
173        st::test_iter_with_prefix(&db)?;
174        fs::remove_dir_all(&db.path)
175    }
176
177    #[test]
178    fn complex() -> io::Result<()> {
179        let db = InFile::open(format!("{:?}", timestramp()), 1)?;
180        st::test_complex(&db)?;
181        fs::remove_dir_all(&db.path)
182    }
183}