gravitydb-filestore 0.3.1

A filestore backend for the gravity database
Documentation
use vfs::{MemoryFS, PhysicalFS, VfsError, VfsPath, VfsResult};
use gravitydb::KVStore;
use std::path::Path;
use thiserror::Error;
pub mod cli_helpers;

pub struct FsKvStore {
  base_path: VfsPath,
}

impl KVStore<FileStoreError> for FsKvStore
{
  fn create_bucket(&mut self, key: &[u8]) -> Result<(), FileStoreError> {
    Ok(self.key_to_path(key)?.create_dir_all()?)
  }

  fn delete_record(&mut self, key: &[u8]) -> Result<(), FileStoreError> {
    Ok(self.key_to_path(key)?.remove_file()?)
  }

  fn store_record(&mut self, key: &[u8], value: &[u8]) -> Result<(), FileStoreError> {
    Ok(self.key_to_path(key)?.create_file()?.write_all(value)?)
  }

  fn fetch_record(&self, key: &[u8]) -> Result<Vec<u8>, FileStoreError> {
    let mut content = vec![];
    self.key_to_path(key)?.open_file()?.read_to_end(&mut content)?;
    Ok(content)
  }

  fn list_records(&self, from: &[u8], to: &[u8]) -> Result<Vec<Vec<u8>>, FileStoreError> {
    let to_path = if to.len() != 0 {
      self.key_to_path(to)?
    } else {
      let mut to: Vec<u8> = from.to_vec();
      *to.last_mut().unwrap() += 1;
      self.key_to_path(&to)?
    };
    let from_path = self.key_to_path(from)?;
    let base = match longest_shared_path(&from_path, &to_path) {
      Some(base) => base,
      None => return Err(FileStoreError::InvalidParameters),
    };
    Ok(list_files(&base)?
      .into_iter()
      .filter(|key| **key < *from || **key > *to)
      .collect())
  }

  fn exists(&self, key: &[u8]) -> Result<bool, FileStoreError> {
    Ok(self.key_to_path(key)?.exists()?)
  }
}

impl FsKvStore {
  fn key_to_path(&self, key: &[u8]) -> Result<VfsPath, FileStoreError> {
    let mut path = self.base_path.clone();
    for component in String::from_utf8_lossy(key).split("/") {
      path = path.join(component)?;
    }
    Ok(path)
  }

  pub fn open(path: &Path) -> Result<Self, FileStoreError> {
    let root = VfsPath::new(PhysicalFS::new(path.to_path_buf()));

    if !root.is_dir()? {
      return Err(FileStoreError::MalformedDB);
    }

    let check_dirs = ["nodes", "edges", "props", "indexes"];
    for dir in &check_dirs {
      if !root.join(dir)?.is_dir()? {
        return Err(FileStoreError::MalformedDB);
      }
    }

    Ok(FsKvStore {
      base_path: root,
    })
  }

  pub fn init(path: &Path) -> Result<Self, FileStoreError> {
    let root = VfsPath::new(PhysicalFS::new(path.to_path_buf()));
    if !root.is_dir()? {
      if root.exists()? {
        return Err(FileStoreError::MalformedDB);
      } else {
        root.create_dir_all()?;
      }
    }

    let check_dirs = ["nodes", "edges", "props", "indexes"];
    for dir in &check_dirs {
      root.join(dir)?.create_dir_all()?;
    }

    Ok(FsKvStore {
      base_path: root,
    })
  }

  pub fn from_memory() -> Result<Self, FileStoreError> {
    let root = VfsPath::new(MemoryFS::new());

    let check_dirs = ["nodes", "edges", "props", "indexes"];
    for dir in &check_dirs {
      root.join(dir)?.create_dir_all()?;
    }

    Ok(FsKvStore { base_path: root })
  }

  pub fn get_root(self) -> VfsPath {
    self.base_path
  }
}

#[derive(Error, Debug)]
pub enum FileStoreError {
  #[error("wrongly formatted database at path TODO")]
  MalformedDB,
  #[error("io error")]
  Io { #[from] source: std::io::Error },
  #[error("vfs error")]
  Vfs { #[from] source: VfsError },
  #[error("invalid input parameters")]
  InvalidParameters,
}

fn list_files(dir: &VfsPath) -> VfsResult<Vec<Vec<u8>>> {
  let mut result = vec![];

  if dir.is_dir()? {
    for path in dir.read_dir()? {
      if path.is_dir()? {
        result.append(&mut list_files(&path)?);
      } else {
        let path = path.as_str();
        let path = match path.strip_prefix("/") {
          Some(path) => path,
          None => path,
        };
        result.push(path.as_bytes().to_vec());
      }
    }
  }
  Ok(result)
}

fn longest_shared_path(path1: &VfsPath, path2: &VfsPath) -> Option<VfsPath> {
  let s1 = path1.as_str();
  let s2 = path2.as_str();

  let mut shared = String::new();

  for (c1, c2) in s1.chars().zip(s2.chars()) {
    if c1 == c2 {
      shared.push(c1);
    } else {
      break;
    }
  }

  if !shared.is_empty() {
    let shared = path1.root().join(shared).ok()?;
    if shared.is_dir().ok()? {
      Some(shared)
    } else {
      Some(shared.parent())
    }
  } else {
    None
  }
}