293 lines
8.7 KiB
Rust
293 lines
8.7 KiB
Rust
|
use std::convert::TryFrom;
|
||
|
|
||
|
use anyhow::{anyhow, Result};
|
||
|
use diesel::sqlite::Sqlite;
|
||
|
use diesel::Connection;
|
||
|
use log::trace;
|
||
|
use serde_json::Value;
|
||
|
use uuid::Uuid;
|
||
|
|
||
|
use crate::addressing::Address;
|
||
|
use crate::database::constants::{
|
||
|
HIER_ADDR, HIER_HAS_ATTR, HIER_INVARIANT, IS_OF_TYPE_ATTR, TYPE_ADDR, TYPE_HAS_ATTR,
|
||
|
};
|
||
|
use crate::database::entry::{Entry, EntryValue};
|
||
|
use crate::database::lang::{EntryQuery, Query, QueryComponent, QueryPart};
|
||
|
use crate::database::{bulk_retrieve_objects, insert_entry, query, DbPool};
|
||
|
|
||
|
#[derive(Debug, Clone, PartialEq)]
|
||
|
pub struct UNode(pub String);
|
||
|
|
||
|
#[derive(Debug, Clone, PartialEq)]
|
||
|
pub struct UPath(pub Vec<UNode>);
|
||
|
|
||
|
impl std::str::FromStr for UPath {
|
||
|
type Err = anyhow::Error;
|
||
|
|
||
|
fn from_str(string: &str) -> Result<Self, Self::Err> {
|
||
|
if string.is_empty() {
|
||
|
Ok(UPath(vec![]))
|
||
|
} else {
|
||
|
let result: Vec<UNode> = string
|
||
|
.trim_end_matches('/')
|
||
|
.split('/')
|
||
|
.map(|part| UNode(String::from(part)))
|
||
|
.collect();
|
||
|
|
||
|
for directory in &result {
|
||
|
if directory.0.is_empty() {
|
||
|
return Err(anyhow!("INVALID PATH: Directory name cannot be empty!"));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Ok(UPath(result))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl std::fmt::Display for UNode {
|
||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
write!(f, "{}", self.0)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl std::fmt::Display for UPath {
|
||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
write!(
|
||
|
f,
|
||
|
"{}",
|
||
|
self.0
|
||
|
.iter()
|
||
|
.map(|node| node.to_string())
|
||
|
.collect::<Vec<String>>()
|
||
|
.join("/")
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
trait EntryList {
|
||
|
fn extract_addresses(&self) -> Vec<Address>;
|
||
|
}
|
||
|
|
||
|
impl EntryList for Vec<Entry> {
|
||
|
fn extract_addresses(&self) -> Vec<Address> {
|
||
|
self.iter()
|
||
|
.filter_map(|e| {
|
||
|
if let EntryValue::Address(address) = &e.value {
|
||
|
Some(address.clone())
|
||
|
} else {
|
||
|
None
|
||
|
}
|
||
|
})
|
||
|
.collect()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn list_roots<C: Connection<Backend = Sqlite>>(connection: &C) -> Result<Vec<Address>> {
|
||
|
let all_directories: Vec<Entry> = query(
|
||
|
connection,
|
||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||
|
entity: QueryComponent::Any,
|
||
|
attribute: QueryComponent::Exact(IS_OF_TYPE_ATTR.to_string()),
|
||
|
value: QueryComponent::Exact(EntryValue::Address(HIER_ADDR.clone())),
|
||
|
})),
|
||
|
)?;
|
||
|
|
||
|
let directories_with_parents: Vec<Address> = query(
|
||
|
connection,
|
||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||
|
entity: QueryComponent::Any,
|
||
|
attribute: QueryComponent::Exact(HIER_HAS_ATTR.to_string()),
|
||
|
value: QueryComponent::Any,
|
||
|
})),
|
||
|
)?
|
||
|
.extract_addresses();
|
||
|
|
||
|
Ok(all_directories
|
||
|
.into_iter()
|
||
|
.filter(|entry| !directories_with_parents.contains(&entry.entity))
|
||
|
.map(|e| e.entity)
|
||
|
.collect())
|
||
|
}
|
||
|
|
||
|
pub async fn list_node<C: Connection<Backend = Sqlite>>(
|
||
|
connection: &C,
|
||
|
path: &UPath,
|
||
|
) -> Result<Vec<Entry>> {
|
||
|
let entry_addresses = if path.0.is_empty() {
|
||
|
list_roots(connection)?
|
||
|
} else {
|
||
|
let resolved_path: Vec<Address> = resolve_path(connection, path, false)?;
|
||
|
let last = resolved_path.last().unwrap();
|
||
|
|
||
|
query(
|
||
|
connection,
|
||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||
|
entity: QueryComponent::Exact(last.clone()),
|
||
|
attribute: QueryComponent::Exact(HIER_HAS_ATTR.to_string()),
|
||
|
value: QueryComponent::Any,
|
||
|
})),
|
||
|
)?
|
||
|
.extract_addresses()
|
||
|
};
|
||
|
|
||
|
Ok(bulk_retrieve_objects(connection, entry_addresses)?
|
||
|
.into_iter()
|
||
|
// .filter(|e| [DIR_KEY, FILENAME_KEY, FILE_IDENTITY_KEY].contains(&e.attribute.as_str()))
|
||
|
.collect::<Vec<Entry>>())
|
||
|
}
|
||
|
|
||
|
pub fn fetch_or_create_dir<C: Connection<Backend = Sqlite>>(
|
||
|
connection: &C,
|
||
|
parent: Option<Address>,
|
||
|
directory: UNode,
|
||
|
create: bool,
|
||
|
) -> Result<Address> {
|
||
|
match parent.clone() {
|
||
|
Some(address) => trace!("FETCHING/CREATING {}/{:#}", address, directory),
|
||
|
None => trace!("FETCHING/CREATING /{:#}", directory),
|
||
|
}
|
||
|
|
||
|
let matching_directories: Vec<Address> = query(
|
||
|
connection,
|
||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||
|
entity: QueryComponent::Any,
|
||
|
attribute: QueryComponent::Exact(String::from(HIER_HAS_ATTR)),
|
||
|
value: QueryComponent::Exact(EntryValue::Value(Value::String(directory.0.clone()))),
|
||
|
})),
|
||
|
)?
|
||
|
.into_iter()
|
||
|
.map(|e: Entry| e.entity)
|
||
|
.collect();
|
||
|
|
||
|
let valid_directories: Vec<Address> = match parent.clone() {
|
||
|
Some(address) => {
|
||
|
let parent_has: Vec<Address> = query(
|
||
|
connection,
|
||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||
|
entity: QueryComponent::Exact(address),
|
||
|
attribute: QueryComponent::Exact(String::from(HIER_HAS_ATTR)),
|
||
|
value: QueryComponent::Any,
|
||
|
})),
|
||
|
)?
|
||
|
.extract_addresses();
|
||
|
|
||
|
matching_directories
|
||
|
.into_iter()
|
||
|
.filter(|a| parent_has.contains(a))
|
||
|
.collect()
|
||
|
}
|
||
|
None => {
|
||
|
let roots = list_roots(connection)?;
|
||
|
matching_directories
|
||
|
.into_iter()
|
||
|
.filter(|a| roots.contains(a))
|
||
|
.collect()
|
||
|
}
|
||
|
};
|
||
|
|
||
|
match valid_directories.len() {
|
||
|
0 => {
|
||
|
if create {
|
||
|
let new_directory_address = Address::Uuid(Uuid::new_v4());
|
||
|
let type_entry = Entry {
|
||
|
entity: new_directory_address.clone(),
|
||
|
attribute: String::from(IS_OF_TYPE_ATTR),
|
||
|
value: EntryValue::Address(HIER_ADDR.clone()),
|
||
|
};
|
||
|
insert_entry(connection, type_entry)?;
|
||
|
|
||
|
let directory_entry = Entry {
|
||
|
entity: new_directory_address.clone(),
|
||
|
attribute: String::from(HIER_HAS_ATTR),
|
||
|
value: EntryValue::Value(Value::String(directory.0)),
|
||
|
};
|
||
|
insert_entry(connection, directory_entry)?;
|
||
|
|
||
|
if let Some(parent_addr) = parent {
|
||
|
let has_entry = Entry {
|
||
|
entity: parent_addr,
|
||
|
attribute: String::from(HIER_HAS_ATTR),
|
||
|
value: EntryValue::Address(new_directory_address.clone()),
|
||
|
};
|
||
|
insert_entry(connection, has_entry)?;
|
||
|
}
|
||
|
|
||
|
Ok(new_directory_address)
|
||
|
} else {
|
||
|
Err(anyhow!("Directory does not exist."))
|
||
|
}
|
||
|
}
|
||
|
1 => Ok(valid_directories[0].clone()),
|
||
|
_ => Err(anyhow!(
|
||
|
"Invalid database state - more than one directory matches the query!"
|
||
|
)),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn resolve_path<C: Connection<Backend = Sqlite>>(
|
||
|
connection: &C,
|
||
|
path: &UPath,
|
||
|
create: bool,
|
||
|
) -> Result<Vec<Address>> {
|
||
|
let mut result: Vec<Address> = vec![];
|
||
|
let mut path_stack = path.0.to_vec();
|
||
|
|
||
|
path_stack.reverse();
|
||
|
while !path_stack.is_empty() {
|
||
|
let dir_address = fetch_or_create_dir(
|
||
|
connection,
|
||
|
result.last().cloned(),
|
||
|
path_stack.pop().unwrap(),
|
||
|
create,
|
||
|
)?;
|
||
|
result.push(dir_address);
|
||
|
}
|
||
|
|
||
|
Ok(result)
|
||
|
}
|
||
|
|
||
|
pub fn initialize_hier(pool: &DbPool) -> Result<()> {
|
||
|
insert_entry(&pool.get()?, Entry::try_from(&*HIER_INVARIANT)?)?;
|
||
|
upend_insert_addr!(&pool.get()?, HIER_ADDR, IS_OF_TYPE_ATTR, TYPE_ADDR);
|
||
|
upend_insert_val!(&pool.get()?, HIER_ADDR, TYPE_HAS_ATTR, HIER_HAS_ATTR);
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod tests {
|
||
|
use anyhow::Result;
|
||
|
|
||
|
use super::{UNode, UPath};
|
||
|
|
||
|
#[test]
|
||
|
fn test_path_codec() {
|
||
|
let path = UPath(vec![
|
||
|
UNode("top".to_string()),
|
||
|
UNode("foo".to_string()),
|
||
|
UNode("bar".to_string()),
|
||
|
UNode("baz".to_string()),
|
||
|
]);
|
||
|
|
||
|
let str_path = path.to_string();
|
||
|
assert!(str_path.len() > 0);
|
||
|
|
||
|
let decoded_path: Result<UPath> = str_path.parse();
|
||
|
assert!(decoded_path.is_ok());
|
||
|
|
||
|
assert_eq!(path, decoded_path.unwrap());
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn test_validation() {
|
||
|
let valid_path: Result<UPath> = "a/b/c/d/e/f/g".parse();
|
||
|
assert!(valid_path.is_ok());
|
||
|
|
||
|
let invalid_path: Result<UPath> = "a/b/c//d/e/f/g".parse();
|
||
|
assert!(invalid_path.is_err());
|
||
|
|
||
|
let invalid_path: Result<UPath> = "a//b/c//d/e/f///g".parse();
|
||
|
assert!(invalid_path.is_err());
|
||
|
}
|
||
|
}
|