1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
use crate::{ForcepError, Result};
use std::io;
use std::path;
use std::time;

/// Type definition for an array of bytes that make up an `md5` hash.
pub type Md5Bytes = [u8; 16];

/// Metadata information about a certain entry in the cache
///
/// This metadata contains information about when the entry was last modified, the size (in bytes)
/// of the entry, the `md5` integrity of the entry, etc.
///
/// # Examples
///
/// ```rust
/// # #[tokio::main(flavor = "current_thread")]
/// # async fn main() {
/// use forceps::Cache;
///
/// let cache = Cache::new("./cache")
///     .build()
///     .await
///     .unwrap();
///
/// cache.write(&b"MY_KEY", &b"Hello World").await.unwrap();
///
/// let metadata = cache.read_metadata(&b"MY_KEY").unwrap();
/// # }
/// ```
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Metadata {
    /// Size in bytes of the corresponding entry
    size: u64,
    /// Last time this entry was modified, milliseconds since epoch
    last_modified: u64,
    /// Last time since this entry was accessed, milliseconds since epoch
    last_accessed: u64,
    /// Number of times this entry has been HIT (total accesses)
    hits: u64,
    /// Md5 hash of the underlying data
    integrity: Md5Bytes,
}

/// Database for cache entry metadata
#[derive(Debug)]
pub(crate) struct MetaDb {
    db: sled::Db,
}

/// Milliseconds from epoch to now
fn now_since_epoch() -> u64 {
    time::SystemTime::now()
        .duration_since(time::UNIX_EPOCH)
        .map(|x| x.as_millis() as u64)
        .unwrap_or(0)
}

impl Metadata {
    /// Creates a new instance of [`Metadata`] from the given `data`
    pub(crate) fn new(data: &[u8]) -> Self {
        Self {
            size: data.len() as u64,
            last_modified: now_since_epoch(),
            last_accessed: now_since_epoch(),
            hits: 0,
            integrity: md5::compute(data).into(),
        }
    }

    /// Serializes the metadata into bytes
    pub(crate) fn serialize(&self) -> Result<Vec<u8>> {
        let document = bson::to_document(self).map_err(ForcepError::MetaSer)?;

        // write document contents to memory stream
        let mut buf = Vec::<u8>::new();
        let mut writer = io::Cursor::new(&mut buf);
        document
            .to_writer(&mut writer)
            .map_err(ForcepError::MetaSer)?;

        Ok(buf)
    }

    /// Deserializes a slice of bytes into metadata
    pub(crate) fn deserialize(buf: &[u8]) -> Result<Self> {
        // create a reader so we can convert the document
        let mut cursor = io::Cursor::new(buf);
        let document = bson::Document::from_reader(&mut cursor).map_err(ForcepError::MetaDe)?;

        bson::from_document(document).map_err(ForcepError::MetaDe)
    }

    /// The size in bytes of the corresponding cache entry.
    #[inline]
    pub fn get_size(&self) -> u64 {
        self.size
    }

    /// Retrives the last time this entry was modified.
    pub fn get_last_modified(&self) -> Option<time::SystemTime> {
        match self.last_modified {
            0 => None,
            millis => Some(time::UNIX_EPOCH + time::Duration::from_millis(millis)),
        }
    }
    /// Retrieves the raw `last_modified` time, which is the milliseconds since
    /// [`time::UNIX_EPOCH`]. If the returned result is `0`, that means there is no `last_modified`
    /// time.
    #[inline]
    pub fn get_last_modified_raw(&self) -> u64 {
        self.last_modified
    }

    /// The total number of times this entry has been read.
    ///
    /// **NOTE:** This will be 0 unless `track_access` is enabled from the [`CacheBuilder`]
    ///
    /// [`CacheBuilder`]: crate::CacheBuilder
    #[inline]
    pub fn get_hits(&self) -> u64 {
        self.hits
    }

    /// Retrives the last time this entry was accessed (read from).
    ///
    /// **NOTE:** This will be the same as [`get_last_modified`] unless `track_access` is enabled from
    /// the [`CacheBuilder`]
    ///
    /// [`get_last_modified`]: Self::get_last_modified
    /// [`CacheBuilder`]: crate::CacheBuilder
    pub fn get_last_acccessed(&self) -> Option<time::SystemTime> {
        match self.last_accessed {
            0 => None,
            millis => Some(time::UNIX_EPOCH + time::Duration::from_millis(millis)),
        }
    }
    /// Retrieves the raw `last_accessed` time, which is the milliseconds since
    /// [`time::UNIX_EPOCH`]. If the returned result is `0`, that means there is no `last_accessed`
    /// time.
    ///
    /// **NOTE:** This will be the same as [`get_last_modified_raw`] unless `track_access` is enabled
    /// from the [`CacheBuilder`]
    ///
    /// [`get_last_modified_raw`]: Self::get_last_modified_raw
    /// [`CacheBuilder`]: crate::CacheBuilder
    #[inline]
    pub fn get_last_accessed_raw(&self) -> u64 {
        self.last_accessed
    }

    /// Retrieves the internal [`Md5Bytes`] integrity of the corresponding metadata entry.
    #[inline]
    pub fn get_integrity(&self) -> &Md5Bytes {
        &self.integrity
    }

    /// Verifies that the metadata integrity matches the integrity of the data provided.
    #[inline]
    pub fn check_integrity_of(&self, data: &[u8]) -> bool {
        let other_integrity: Md5Bytes = md5::compute(data).into();
        other_integrity == self.integrity
    }
}

impl MetaDb {
    /// Initializes a new metadata database with sled.
    pub fn new(path: &path::Path) -> Result<Self> {
        sled::open(path)
            .map_err(ForcepError::MetaDb)
            .map(|db| Self { db })
    }

    /// Retrieves an entry in the metadata database with the corresponding key.
    pub fn get_metadata(&self, key: &[u8]) -> Result<Metadata> {
        let data = match self.db.get(key) {
            Ok(Some(data)) => data,
            Ok(None) => return Err(ForcepError::MetaNotFound),
            Err(e) => return Err(ForcepError::MetaDb(e)),
        };
        Metadata::deserialize(&data)
    }

    /// Inserts a new entry into the metadata database for the associated key and data.
    ///
    /// If a previous entry exists, it is simply overwritten.
    pub fn insert_metadata_for(&self, key: &[u8], data: &[u8]) -> Result<Metadata> {
        let meta = Metadata::new(data);
        let bytes = Metadata::serialize(&meta)?;
        self.db
            .insert(key, &bytes[..])
            .map_err(ForcepError::MetaDb)?;
        Ok(meta)
    }

    pub fn remove_metadata_for(&self, key: &[u8]) -> Result<Metadata> {
        match self.db.remove(key) {
            Ok(Some(m)) => Metadata::deserialize(&m[..]),
            Ok(None) => Err(ForcepError::MetaNotFound),
            Err(e) => Err(ForcepError::MetaDb(e)),
        }
    }

    /// Will increment the `hits` counter and set the `last_accessed` value to now for the found
    /// metadata key.
    pub fn track_access_for(&self, key: &[u8]) -> Result<Metadata> {
        let mut meta = match self.db.get(key) {
            Ok(Some(entry)) => Metadata::deserialize(&entry[..])?,
            Err(e) => return Err(ForcepError::MetaDb(e)),
            Ok(None) => return Err(ForcepError::MetaNotFound),
        };
        meta.last_accessed = now_since_epoch();
        meta.hits += 1;
        self.db
            .insert(key, Metadata::serialize(&meta)?)
            .map_err(ForcepError::MetaDb)?;
        Ok(meta)
    }

    /// Iterator over the entire metadata database
    pub fn metadata_iter(&self) -> impl Iterator<Item = Result<(Vec<u8>, Metadata)>> {
        self.db.iter().map(|x| match x {
            Ok((key, data)) => Metadata::deserialize(&data[..]).map(|m| (key.to_vec(), m)),
            Err(e) => Err(ForcepError::MetaDb(e)),
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;
    const DATA: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];

    fn create_db() -> Result<MetaDb> {
        const META_TESTDIR: &str = "./cache/test-index";
        let path = path::PathBuf::from(META_TESTDIR);
        MetaDb::new(&path)
    }

    #[test]
    fn create_metadb() {
        create_db().unwrap();
    }

    #[test]
    fn db_read_write() {
        let db = create_db().unwrap();
        db.insert_metadata_for(&DATA, &DATA).unwrap();
        let meta = db.get_metadata(&DATA).unwrap();
        assert_eq!(meta.get_size(), DATA.len() as u64);
    }

    #[test]
    fn check_integrity() {
        let db = create_db().unwrap();
        let meta = db.insert_metadata_for(&DATA, &DATA).unwrap();
        assert!(meta.check_integrity_of(&DATA));
    }

    #[test]
    fn last_modified() {
        let db = create_db().unwrap();
        let meta = db.insert_metadata_for(&DATA, &DATA).unwrap();
        // make sure last-modified date is within last second
        assert!(
            meta.get_last_modified()
                .unwrap()
                .elapsed()
                .unwrap()
                .as_secs()
                == 0
        );
    }

    #[test]
    fn metadata_ser_de() {
        let db = create_db().unwrap();
        let meta = db.insert_metadata_for(&DATA, &DATA).unwrap();
        let ser_bytes = meta.serialize().unwrap();
        let de = Metadata::deserialize(&ser_bytes).unwrap();
        assert_eq!(meta.get_integrity(), de.get_integrity());
    }
}