mammon/
lib.rs

1// mammon - a storage engine
2// `store(key: ToString, val: Iter<u8>): Result<()>` and `retrieve(key: ToString): Result<Iter<u8>>` and maybe `defragement(): Result<>`
3
4use std::collections::HashMap;
5use std::fs::{create_dir_all, File, OpenOptions};
6use std::io::{Read, Seek, Write};
7use std::path::{Path, PathBuf};
8
9use anyhow::{bail, Result};
10use serde::{Deserialize, Serialize};
11
12#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
13pub struct Index {
14    pub offset: u64,
15    pub length: u64,
16}
17
18pub struct Store {
19    pub indexes: HashMap<String, Index>,
20    pub empties: Vec<Index>,
21    pub blob_file: File,
22    pub empty_file: File,
23    pub db_file: File,
24}
25/// open a file with r/w permissions.
26fn open_file<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
27    OpenOptions::new()
28        .read(true)
29        .write(true)
30        .open(path.as_ref())
31}
32
33/// create a file with r/w permissions, truncating it if it exists
34fn create_file<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
35    OpenOptions::new()
36        .read(true)
37        .write(true)
38        .create(true)
39        .truncate(true)
40        .open(path.as_ref())
41}
42
43impl Store {
44    /// creates a *new* Mammon::Store in the given directory. cannot be used to open an existing store.
45    pub fn new(directory: PathBuf) -> Result<Self> {
46        if !directory.exists() {
47            create_dir_all(&directory)?;
48        } else if !directory.is_dir() {
49            bail!("{:?} is not a directory", directory);
50        }
51
52        let blob_file = create_file(directory.join("mammon_blobs.bin"))?;
53        let empty_file = create_file(directory.join("mammon_empties.cbor"))?;
54        let db_file = create_file(directory.join("mammon.cbor"))?;
55
56        Ok(Self {
57            indexes: HashMap::new(),
58            empties: vec![],
59            blob_file,
60            empty_file,
61            db_file,
62        })
63    }
64    /// opens an existing Mammon::Store in a given directory. cannot be used to create a new store.
65    pub fn open(directory: PathBuf) -> Result<Self> {
66        if !directory.exists() {
67            bail!("{:?} does not exist", directory);
68        }
69
70        let blob_file = open_file(directory.join("mammon_blobs.bin"))?;
71        let empty_file = open_file(directory.join("mammon_empties.cbor"))?;
72        let db_file = open_file(directory.join("mammon.cbor"))?;
73
74        let indexes: HashMap<String, Index> = ciborium::from_reader(&db_file)?;
75        let empties: Vec<Index> = ciborium::from_reader(&empty_file)?;
76
77        Ok(Self {
78            indexes,
79            empties,
80            blob_file,
81            empty_file,
82            db_file,
83        })
84    }
85
86    /// store a blob in the store, returning Ok(()) on success
87    pub fn store(&mut self, key: impl ToString, val: Vec<u8>) -> Result<()> {
88        let offset = self.blob_file.seek(std::io::SeekFrom::End(0))?;
89        let length = val.len() as u64;
90
91        self.blob_file.write_all(val.as_slice())?;
92
93        self.indexes
94            .insert(key.to_string(), Index { offset, length });
95
96        ciborium::into_writer(&self.indexes, &self.db_file)?;
97
98        Ok(())
99    }
100
101    /// retrieve a blob from the store
102    pub fn retrieve(&mut self, key: impl ToString) -> Result<Vec<u8>> {
103        let index = self
104            .indexes
105            .get(&key.to_string())
106            .ok_or_else(|| anyhow::anyhow!("key not found"))?;
107
108        self.blob_file
109            .seek(std::io::SeekFrom::Start(index.offset))?;
110        let mut buf = vec![0; index.length as usize];
111        self.blob_file.read_exact(&mut buf)?;
112
113        return Ok(buf.clone());
114    }
115
116    /// delete a blob from the store
117    pub fn delete(&mut self, key: impl ToString) -> Result<()> {
118        let index = self
119            .indexes
120            .get(&key.to_string())
121            .ok_or_else(|| anyhow::anyhow!("key not found"))?;
122
123        self.empties.push(Index {
124            offset: index.offset,
125            length: index.length,
126        }); // sigh emoji
127
128        self.indexes.remove(&key.to_string());
129
130        ciborium::into_writer(&self.indexes, &self.db_file)?;
131        ciborium::into_writer(&self.empties, &self.empty_file)?;
132
133        Ok(())
134    }
135
136    // FIXME: implement defragmentation, to avoid the file growing forever
137}