morethantext-web/src/morethantext/entry.rs

376 lines
13 KiB
Rust

use async_std::{fs::{read, remove_file, write}, path::PathBuf};
use super::{DataType, DBError, ErrorCode, FileData, SessionData};
use std::{cell::Cell, time::{Duration, Instant}};
pub struct Entry {
data: DataType,
filename: PathBuf,
last_used: Cell<Instant>,
}
impl Entry {
pub async fn new<P>(filename: P, data: DataType) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
if pathbuf.as_path().exists().await {
return Err(DBError::from_code(ErrorCode::EntryExists(pathbuf)));
} else {
match write(&pathbuf, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryWriteFailure(pathbuf));
error.add_source(err);
return Err(error);
}
}
}
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
pub async fn get<P>(filename: P) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
let content = match read(&pathbuf).await {
Ok(text) => text,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
let data = match DataType::from_bytes(&mut content.iter()) {
Ok(raw) => raw,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
pub fn elapsed(&self) -> Duration {
self.last_used.get().elapsed()
}
pub fn data(&self) -> DataType {
self.last_used.set(Instant::now());
self.data.clone()
}
async fn update(&mut self, data: DataType) -> Result<(), DBError> {
self.last_used.set(Instant::now());
match write(&self.filename, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryWriteFailure(self.filename.clone()));
error.add_source(err);
return Err(error);
}
};
self.data = data;
Ok(())
}
async fn remove(&self) -> Result<(), DBError> {
match remove_file(&self.filename).await {
Ok(_) => Ok(()),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryDeleteFailure(self.filename.clone()));
error.add_source(err);
Err(error)
}
}
}
}
#[cfg(test)]
mod entry {
use super::*;
use std::error::Error;
use tempfile::tempdir;
#[async_std::test]
async fn get_elapsed_time() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("count");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
item.last_used
.set(Instant::now() - Duration::from_secs(500));
assert!(
Duration::from_secs(499) < item.elapsed(),
"The duration should have increased."
);
}
#[async_std::test]
async fn create() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "roger", "moore").unwrap();
let filepath = dir.path().join("wiliam");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
assert!(filepath.is_file(), "Should have created the entry file.");
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn create_errors_on_bad_files() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("bad").join("path");
let filename = filepath.to_str().unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => Err(DBError::new("bad file names should raise an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn create_does_not_over_writes() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let id = "wicked";
let file = dir.path().join(id);
let filename = file.to_str().unwrap();
write(&file, b"previous").await.unwrap();
let data = DataType::new("store").unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => {
return Err(DBError::new(
"Should produce an error for an existing Entry",
))
}
Err(err) => match err.code {
ErrorCode::EntryExists(_) => Ok(()),
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn get_updates_last_used() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("holder");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(300));
item.data();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn update_entry() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(500));
data.add("database", "new", "stuff").unwrap();
item.update(data.clone()).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn update_write_errors() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
drop(dir);
match item.update(data).await {
Ok(_) => Err(DBError::new("file writes should return an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "something_old", "3.14159").unwrap();
let filepath = dir.path().join("existing");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
let output = Entry::get(filename).await.unwrap();
assert_eq!(
output.data().list(["database"].to_vec()).unwrap(),
data.list(["database"].to_vec()).unwrap()
);
assert_eq!(output.filename.to_str().unwrap(), filename);
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn retrieve_file_missing() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("justnotthere");
let filename = filepath.to_str().unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not read file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve_corrupt_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("garbage");
let filename = filepath.to_str().unwrap();
write(&filepath, b"jhsdfghlsdf").await.unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source().unwrap().to_string().contains("corrupt file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn delete() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("byebye");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.remove().await.unwrap();
assert!(!filepath.exists(), "Entry file should be removed.");
}
#[async_std::test]
async fn delete_bad_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("itsnotthere");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
remove_file(filename).await.unwrap();
match item.remove().await {
Ok(_) => Err(DBError::new("should have produced an error")),
Err(err) => match err.code {
ErrorCode::EntryDeleteFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not remove file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
}