use anyhow::{Context, Result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
pub struct KvStore {
file: File,
index: HashMap<String, u64>,
}
impl KvStore {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("Failed to create data directory")?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path)
.context("Failed to open log file")?;
let mut store = KvStore {
file,
index: HashMap::new(),
};
store.load_index()?;
Ok(store)
}
fn load_index(&mut self) -> Result<()> {
let mut reader = BufReader::new(&self.file);
let mut offset = 0;
loop {
let key_len = match reader.read_u32::<LittleEndian>() {
Ok(len) => len as u64,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, Err(e) => return Err(e.into()),
};
let val_len = reader.read_u32::<LittleEndian>().context("Failed to read value length")?;
let mut key_bytes = vec![0u8; key_len as usize];
reader.read_exact(&mut key_bytes).context("Failed to read key")?;
let key = String::from_utf8(key_bytes).context("Key contains invalid UTF-8")?;
if val_len == u32::MAX {
self.index.remove(&key);
offset += 4 + 4 + key_len;
continue;
}
self.index.insert(key, offset);
reader.seek_relative(val_len as i64).context("Failed to skip value")?;
offset += 4 + 4 + key_len + (val_len as u64);
}
Ok(())
}
pub fn remove(&mut self, key: String) -> Result<()> {
let mut writer = BufWriter::new(&self.file);
writer.seek(SeekFrom::End(0)).context("Failed to seek to end")?;
let key_bytes = key.as_bytes();
writer.write_u32::<LittleEndian>(key_bytes.len() as u32).context("Failed to write key len")?;
writer.write_u32::<LittleEndian>(u32::MAX).context("Failed to write tombstone val len")?;
writer.write_all(key_bytes).context("Failed to write key")?;
writer.flush().context("Failed to flush writes")?;
self.index.remove(&key);
Ok(())
}
pub fn set(&mut self, key: String, value: String) -> Result<()> {
let mut writer = BufWriter::new(&self.file);
let current_offset = writer.seek(SeekFrom::End(0)).context("Failed to seek to end")?;
let key_bytes = key.as_bytes();
let val_bytes = value.as_bytes();
writer.write_u32::<LittleEndian>(key_bytes.len() as u32).context("Failed to write key len")?;
writer.write_u32::<LittleEndian>(val_bytes.len() as u32).context("Failed to write val len")?;
writer.write_all(key_bytes).context("Failed to write key")?;
writer.write_all(val_bytes).context("Failed to write value")?;
writer.flush().context("Failed to flush writes")?;
self.index.insert(key, current_offset);
Ok(())
}
pub fn get(&mut self, key: &str) -> Result<Option<String>> {
let offset = match self.index.get(key) {
Some(&o) => o,
None => return Ok(None),
};
let mut reader = BufReader::new(&self.file);
reader.seek(SeekFrom::Start(offset)).context("Failed to seek to record")?;
let key_len = reader.read_u32::<LittleEndian>().context("Failed to read key len")? as u64;
let val_len = reader.read_u32::<LittleEndian>().context("Failed to read val len")? as u64;
reader.seek_relative(key_len as i64).context("Failed to skip key")?;
let mut val_bytes = vec![0u8; val_len as usize];
reader.read_exact(&mut val_bytes).context("Failed to read value")?;
let value = String::from_utf8(val_bytes).context("Value contains invalid UTF-8")?;
Ok(Some(value))
}
}