docserver/src/defs/filestore.rs

368 lines
14 KiB
Rust

use async_session::{Result, Session, SessionStore};
use anyhow::anyhow;
use async_trait::async_trait;
use std::{
fs,
path::Path,
};
use walkdir::{DirEntry, WalkDir};
use binascii::bin2hex;
#[allow(unused)]
fn is_hidden(entry: &DirEntry) -> bool {
entry.file_name()
.to_str()
.map(|s| s.starts_with("."))
.unwrap_or(false)
}
#[derive(Debug, Clone)]
pub struct FileStore {
pub sess_path: String,
pub ses_file: String,
}
#[async_trait]
impl SessionStore for FileStore {
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
log::trace!("loading session by id `{}`", &id);
//dbg!("loading session by id `{}`", &id);
self.load_session_file(&id).await
}
async fn store_session(&self, session: Session) -> Result<Option<String>> {
log::trace!("storing session by id `{}`", session.id());
let id_filename = match self.get_path(session.id()) {
Ok(res) => res,
Err(e) => {
return Err(e);
}
};
// let mut out_buffer = [0u8; 100];
// let id_filename = if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
// std::str::from_utf8(res)?.to_owned()
// } else {
// return Ok(None);
// };
let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
if ! Path::new(&sess_id_path).exists() {
fs::create_dir(&sess_id_path)?;
}
let content_session = serde_json::to_string(&session)?;
fs::write(&format!("{}/{}",&sess_id_path, self.ses_file), content_session)?;
//session.reset_data_changed(); // do not need is it is serialized in file write
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> Result {
log::trace!("destroying session by id `{}`", session.id());
match self.get_path(session.id()) {
Ok(res) => match self.get_session_id_path(&res) {
Ok(session_id_path) => Ok(fs::remove_file(&session_id_path)?),
Err(e) => Err(e),
},
Err(e) => Err(e)
}
// let mut out_buffer = [0u8; 100];
// if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
// let id_filename = std::str::from_utf8(res)?.to_owned();
// Ok(fs::remove_file(
// &format!("{}/{}/{}",self.sess_path, &id_filename, self.ses_file)
// )?)
// } else {
// Ok(())
// }
}
async fn clear_store(&self) -> Result {
log::trace!("clearing memory store");
let sess_path = format!("{}", self.sess_path);
fs::remove_dir_all(&sess_path)?;
fs::create_dir(&sess_path)?;
Ok(())
}
}
impl FileStore {
/// Create a new instance of FilesStore
pub fn check_paths(&self) -> Result {
if ! Path::new(&self.sess_path).exists() {
fs::create_dir(&self.sess_path)?;
}
Ok(())
}
pub fn get_path(&self,id: &str) -> Result<String> {
let mut out_buffer = [0u8; 100];
match bin2hex(&id.as_bytes(),&mut out_buffer) {
Ok(res) => Ok(std::str::from_utf8(res)?.to_owned()),
Err(e) => {
Err(anyhow!("Filename path {} not generated: {:?}", &id, &e))
}
}
}
pub fn get_session_id_path(&self,id_filename: &str) -> Result<String> {
let session_id_path = format!("{}/{}/{}",self.sess_path, id_filename, &self.ses_file);
if ! Path::new(&session_id_path).exists() {
Err(anyhow!("Filename path {} not found: {}", id_filename, &session_id_path ))
} else {
Ok(session_id_path)
}
}
/// As session Id from `async_session` comes in base64 it will be not valid for OS filename
/// `bin2hex` pass id to hex as bytes and from there to string or viceversa
pub async fn load_session_file(&self, id: &str) -> Result<Option<Session>> {
let session_id_path = match self.get_path(id) {
Ok(res) => match self.get_session_id_path(&res) {
Ok(path) => path,
Err(e) => return Err(e),
},
Err(e) => {
return Err(e);
}
};
// let mut out_buffer = [0u8; 100];
// let id_filename = if let Ok(res) = bin2hex(&id.as_bytes(),&mut out_buffer) {
// std::str::from_utf8(res)?.to_owned()
// } else {
// return Ok(None);
// };
// let session_id_path = format!("{}/{}/{}",self.sess_path, &id_filename, &self.ses_file);
// dbg!(&session_id_path);
if ! Path::new(&session_id_path).exists() {
dbg!("No path: {}", &session_id_path);
// let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
// if ! Path::new(&sess_id_path).exists() {
// fs::create_dir(&sess_id_path)?;
// }
// create
}
if let Ok(session_content) = fs::read_to_string(&session_id_path) {
// match serde_json::from_str::<serde_json::Value>(&session_content) {
match serde_json::from_str::<Session>(&session_content) {
Ok(session) => {
Ok(session.validate())
},
Err(e) => {
dbg!("Error loading session content from {}: {}",&session_id_path, e);
//log::error!("Error loading session content from {}: {}",&session_id_path, e);
Ok(None)
}
}
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub async fn cleanup(&self) -> Result {
log::trace!("cleaning up file store...");
let mut count: usize = 0;
let sess_path = format!("{}", self.sess_path);
let walker = WalkDir::new(&sess_path).into_iter();
for entry in walker.filter_entry(|e| !is_hidden(e)) {
match entry {
Ok(dir_entry) => {
// println!("{}", &dir_entry.path().display());
if ! Path::new(&dir_entry.path()).is_dir() {
continue;
}
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
let id_path = format!("{}",&dir_entry.path().display());
let id = id_path.replace(&sess_path,"");
if let Some(session) = self.load_session_file(&id).await.unwrap_or_default() {
if session.is_expired() {
let _ = fs::remove_file(&session_file);
log::trace!("found {} expired session",&id_path);
count +=1;
}
}
},
Err(e) => println!("Error on {}: {}", &sess_path, e)
}
}
log::trace!("found {} expired session {} cleaned",&sess_path, count);
Ok(())
}
#[allow(dead_code)]
pub async fn count(&self) -> usize {
let mut count: usize = 0;
let sess_path = format!("{}", self.sess_path);
let walker = WalkDir::new(&sess_path).into_iter();
for entry in walker.filter_entry(|e| !is_hidden(e)) {
match entry {
Ok(dir_entry) => {
// println!("{}", &dir_entry.path().display());
if ! Path::new(&dir_entry.path()).is_dir() {
continue;
}
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
if Path::new(&session_file).exists() {
count +=1;
}
},
Err(e) => println!("Error on {}: {}", &sess_path, e)
}
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_std::task;
use std::time::Duration;
const TEST_SESS_FILESTORE: &str = "/tmp/test_sess_filestore";
const TEST_IDS_FILESTORE: &str = "/tmp/test_ids_filestore";
#[async_std::test]
async fn creating_a_new_session_with_no_expiry() -> Result {
let sess_path_store = format!("{}_0", TEST_SESS_FILESTORE);
let ids_path_store = format!("{}_0", TEST_IDS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let _ = fs::remove_dir_all(&ids_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
let mut session = Session::new();
session.insert("key", "Hello")?;
let cloned = session.clone();
let cookie_value = store.store_session(session).await?.unwrap();
assert!(true);
let loaded_session = store.load_session(cookie_value).await?.unwrap();
assert_eq!(cloned.id(), loaded_session.id());
assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
assert!(!loaded_session.is_expired());
assert!(loaded_session.validate().is_some());
let _ = fs::remove_dir_all(&sess_path_store);
let _ = fs::remove_dir_all(&ids_path_store);
Ok(())
}
#[async_std::test]
async fn updating_a_session() -> Result {
let sess_path_store = format!("{}_1", TEST_SESS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
let mut session = Session::new();
session.insert("key", "value")?;
let cookie_value = store.store_session(session).await?.unwrap();
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
session.insert("key", "other value")?;
assert_eq!(store.store_session(session).await?, None);
let session = store.load_session(cookie_value).await?.unwrap();
assert_eq!(&session.get::<String>("key").unwrap(), "other value");
fs::remove_dir_all(&sess_path_store)?;
Ok(())
}
#[async_std::test]
async fn updating_a_session_extending_expiry() -> Result {
let sess_path_store = format!("{}_2", TEST_SESS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
let mut session = Session::new();
session.expire_in(Duration::from_secs(1));
let original_expires = session.expiry().unwrap().clone();
let cookie_value = store.store_session(session).await?.unwrap();
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(session.expiry().unwrap(), &original_expires);
session.expire_in(Duration::from_secs(3));
let new_expires = session.expiry().unwrap().clone();
assert_eq!(None, store.store_session(session).await?);
let session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(session.expiry().unwrap(), &new_expires);
task::sleep(Duration::from_secs(3)).await;
assert_eq!(None, store.load_session(cookie_value).await?);
fs::remove_dir_all(&sess_path_store)?;
Ok(())
}
#[async_std::test]
async fn creating_a_new_session_with_expiry() -> Result {
let sess_path_store = format!("{}_3", TEST_SESS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
let mut session = Session::new();
session.expire_in(Duration::from_secs(3));
session.insert("key", "value")?;
let cloned = session.clone();
let cookie_value = store.store_session(session).await?.unwrap();
let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(cloned.id(), loaded_session.id());
assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());
assert!(!loaded_session.is_expired());
task::sleep(Duration::from_secs(3)).await;
assert_eq!(None, store.load_session(cookie_value).await?);
fs::remove_dir_all(&sess_path_store)?;
Ok(())
}
#[async_std::test]
async fn destroying_a_single_session() -> Result {
let sess_path_store = format!("{}_4", TEST_SESS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
for _ in 0..3i8 {
store.store_session(Session::new()).await?;
}
let cookie = store.store_session(Session::new()).await?.unwrap();
assert_eq!(4, store.count().await);
let session = store.load_session(cookie.clone()).await?.unwrap();
store.destroy_session(session.clone()).await?;
assert!(store.load_session(cookie).await.is_err());
assert_eq!(3, store.count().await);
// attempting to destroy the session again IS an ERROR, file should be deleted before
assert!(store.destroy_session(session).await.is_err());
fs::remove_dir_all(&sess_path_store)?;
Ok(())
}
#[async_std::test]
async fn clearing_the_whole_store() -> Result {
let sess_path_store = format!("{}_5", TEST_SESS_FILESTORE);
let _ = fs::remove_dir_all(&sess_path_store);
let store = FileStore {
sess_path: sess_path_store.to_owned(),
ses_file: String::from("session"),
};
store.check_paths()?;
for _ in 0..3i8 {
store.store_session(Session::new()).await?;
}
assert_eq!(3, store.count().await);
store.clear_store().await.unwrap();
assert_eq!(0, store.count().await);
fs::remove_dir_all(&sess_path_store)?;
Ok(())
}
}