use serde::{Deserialize, Serialize}; use std::{ convert::TryFrom, ops::Deref, path::{Path, PathBuf}, }; use thiserror::Error; use uuid::Uuid; mod file; mod fileinfo; mod thumbnail; pub mod utils; pub use file::File; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; #[derive(Debug, Error)] pub enum WriteFileError { #[error("root file path does not exist")] RootNotFound, #[error("permission denied")] PermissionDenied, #[error("invalid path")] InvalidPath, #[error("image conversion failed")] ImageError(#[from] image::ImageError), #[error("JSON error")] JSONError(#[from] serde_json::error::Error), #[error("IO error")] IOError(#[from] std::io::Error), } #[derive(Debug, Error)] pub enum ReadFileError { #[error("file not found")] FileNotFound(PathBuf), #[error("path is not a file")] NotAFile, #[error("permission denied")] PermissionDenied, #[error("invalid path")] InvalidPath, #[error("JSON error")] JSONError(#[from] serde_json::error::Error), #[error("IO error")] IOError(#[from] std::io::Error), } #[derive(Debug, Error)] pub enum PathError { #[error("path cannot be derived from input")] InvalidPath, } #[derive(Clone, Debug)] pub struct PathResolver { base: PathBuf, id: String, extension: String, } impl PathResolver { fn new(base: &Path, id: &str, extension: &str) -> Self { Self { base: base.to_owned(), id: id.to_owned(), extension: extension.to_owned(), } } fn file_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(self.id.clone()); path.set_extension(self.extension.clone()); path } fn metadata_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(self.id.clone()); path.set_extension("json"); path } fn thumbnail_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(self.id.clone()); path.set_extension(format!("tn.{}", self.extension)); path } } impl TryFrom for PathResolver { type Error = PathError; fn try_from(s: String) -> Result { PathResolver::try_from(s.as_str()) } } impl TryFrom<&str> for PathResolver { type Error = PathError; fn try_from(s: &str) -> Result { let path = Path::new(s); PathResolver::try_from(Path::new(s)) } } impl TryFrom for PathResolver { type Error = PathError; fn try_from(path: PathBuf) -> Result { PathResolver::try_from(path.as_path()) } } impl TryFrom<&Path> for PathResolver { type Error = PathError; fn try_from(path: &Path) -> Result { Ok(Self { base: path .parent() .map(|s| s.to_owned()) .ok_or(PathError::InvalidPath)?, id: path .file_stem() .and_then(|s| s.to_str().map(|s| s.to_owned())) .ok_or(PathError::InvalidPath)?, extension: path .extension() .and_then(|s| s.to_str().map(|s| s.to_owned())) .ok_or(PathError::InvalidPath)?, }) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileId(String); impl From for FileId { fn from(s: String) -> Self { Self(s) } } impl From<&str> for FileId { fn from(s: &str) -> Self { Self(s.to_owned()) } } impl Deref for FileId { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } pub trait FileRoot { fn root(&self) -> PathBuf; } pub struct Context(PathBuf); impl FileRoot for Context { fn root(&self) -> PathBuf { self.0.clone() } } pub struct Store { files_root: PathBuf, } impl Store { pub fn new(files_root: PathBuf) -> Self { Self { files_root } } pub fn list_files(&self) -> Vec> { unimplemented!() } pub fn add_file(&mut self, filename: String, content: Vec) -> Result { let context = Context(self.files_root.clone()); let mut file = File::new(filename, context)?; file.set_content(content)?; Ok(file) } pub fn delete_file(&mut self, id: FileId) -> Result<(), WriteFileError> { /* let f = File::open(&id, &self.files_root)?; f.delete() */ unimplemented!() } pub fn get_metadata(&self, id: FileId) -> Result { let mut path = self.files_root.clone(); path.push(PathBuf::from((*id).clone())); path.set_extension("json"); FileInfo::load(path) } pub fn get_file(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { let info = self.get_metadata(id.clone())?; let resolver = PathResolver { base: self.files_root.clone(), id: (*id).clone(), extension: info.extension.clone(), }; let f = File::load(resolver)?; Ok((info, f)) } pub fn get_thumbnail(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { unimplemented!() } } #[cfg(test)] mod test { use super::*; use crate::store::utils::FileCleanup; use cool_asserts::assert_matches; use std::io::Read; #[test] fn paths() { let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") .expect("to have a valid path"); assert_eq!( resolver.file_path(), PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") ); assert_eq!( resolver.metadata_path(), PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") ); assert_eq!( resolver.thumbnail_path(), PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png") ); } #[test] fn adds_files() { let mut buf = Vec::new(); let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); file.read_to_end(&mut buf).unwrap(); let mut store = Store::new(PathBuf::from("var/")); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); store .get_file(file_record.id.clone()) .expect("to retrieve the file"); assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); } #[test] fn sets_up_metadata_for_file() { let mut buf = Vec::new(); let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); file.read_to_end(&mut buf).unwrap(); let mut store = Store::new(PathBuf::from("var/")); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); let info = store .get_metadata(file_record.id.clone()) .expect("to retrieve the file"); assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { assert_eq!(size, 23777); assert_eq!(file_type, "image/png"); assert_eq!(hash, "".to_owned()); assert_eq!(extension, "png".to_owned()); }); assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); } #[test] fn sets_up_thumbnail_for_file() {} #[test] fn deletes_associated_files() {} #[test] fn lists_files_in_the_db() {} }