use serde::{Deserialize, Serialize}; use std::{collections::HashSet, ops::Deref, path::PathBuf}; use thiserror::Error; mod filehandle; mod fileinfo; pub use filehandle::FileHandle; pub use fileinfo::FileInfo; #[derive(Debug, Error)] pub enum WriteFileError { #[error("root file path does not exist")] RootNotFound, #[error("permission denied")] PermissionDenied, #[error("invalid path")] InvalidPath, #[error("no metadata available")] NoMetadata, #[error("file could not be loaded")] LoadError(#[from] ReadFileError), #[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("JSON error")] JSONError(#[from] serde_json::error::Error), #[error("IO error")] IOError(#[from] std::io::Error), } #[derive(Debug, Error)] pub enum DeleteFileError { #[error("file not found")] FileNotFound(PathBuf), #[error("metadata path is not a file")] NotAFile, #[error("cannot read metadata")] PermissionDenied, #[error("invalid metadata path")] MetadataParseError(serde_json::error::Error), #[error("IO error")] IOError(#[from] std::io::Error), } impl From for DeleteFileError { fn from(err: ReadFileError) -> Self { match err { ReadFileError::FileNotFound(path) => DeleteFileError::FileNotFound(path), ReadFileError::NotAFile => DeleteFileError::NotAFile, ReadFileError::PermissionDenied => DeleteFileError::PermissionDenied, ReadFileError::JSONError(err) => DeleteFileError::MetadataParseError(err), ReadFileError::IOError(err) => DeleteFileError::IOError(err), } } } #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] 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 From for PathBuf { fn from(s: FileId) -> Self { Self::from(&s) } } impl From<&FileId> for PathBuf { fn from(s: &FileId) -> Self { let FileId(s) = s; Self::from(s) } } 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) -> Result, ReadFileError> { let paths = std::fs::read_dir(&self.files_root)?; let info_files = paths .into_iter() .filter_map(|path| { let path_ = path.unwrap().path(); if path_.extension().and_then(|s| s.to_str()) == Some("json") { let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap(); Some(FileId::from(stem)) } else { None } }) .collect::>(); Ok(info_files) } pub fn add_file( &mut self, filename: String, content: Vec, ) -> Result { let mut file = FileHandle::new(filename, self.files_root.clone())?; file.set_content(content)?; Ok(file) } pub fn get_file(&self, id: &FileId) -> Result { FileHandle::load(id, &self.files_root) } pub fn delete_file(&mut self, id: &FileId) -> Result<(), DeleteFileError> { let handle = FileHandle::load(id, &self.files_root)?; handle.delete(); Ok(()) } pub fn get_metadata(&self, id: &FileId) -> Result { let mut path = self.files_root.clone(); path.push(PathBuf::from(id)); path.set_extension("json"); FileInfo::load(path) } } #[cfg(test)] mod test { use super::*; use cool_asserts::assert_matches; use std::{collections::HashSet, io::Read}; use tempdir::TempDir; fn with_file(test_fn: F) where F: FnOnce(Store, FileId, TempDir), { let tmp = TempDir::new("var").unwrap(); 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(tmp.path())); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); test_fn(store, file_record.id, tmp); } #[test] fn adds_files() { with_file(|store, id, tmp| { let file = store.get_file(&id).expect("to retrieve the file"); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); assert!(tmp.path().join(&(*id)).with_extension("png").exists()); assert!(tmp.path().join(&(*id)).with_extension("json").exists()); assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists()); }); } #[test] fn sets_up_metadata_for_file() { with_file(|store, id, tmp| { assert!(tmp.path().join(&(*id)).with_extension("png").exists()); let info = store.get_metadata(&id).expect("to retrieve the metadata"); assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { assert_eq!(size, 23777); assert_eq!(file_type, "image/png"); assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned()); assert_eq!(extension, "png".to_owned()); }); }); } /* #[test] fn sets_up_thumbnail_for_file() { with_file(|store, id| { let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail"); assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869); }); } */ #[test] fn deletes_associated_files() { with_file(|mut store, id, tmp| { store.delete_file(&id).expect("file to be deleted"); assert!(!tmp.path().join(&(*id)).with_extension("png").exists()); assert!(!tmp.path().join(&(*id)).with_extension("json").exists()); assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists()); }); } #[test] fn lists_files_in_the_db() { with_file(|store, id, _| { let resolvers = store.list_files().expect("file listing to succeed"); let ids = resolvers.into_iter().collect::>(); assert_eq!(ids.len(), 1); assert!(ids.contains(&id)); }); } }