use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError}; use chrono::prelude::*; use hex_string::HexString; use image::imageops::FilterType; use sha2::{Digest, Sha256}; use std::{ convert::TryFrom, io::{Read, Write}, path::{Path, PathBuf}, }; use thiserror::Error; use uuid::Uuid; #[derive(Debug, Error)] pub enum PathError { #[error("path cannot be derived from input")] InvalidPath, } #[derive(Clone, Debug)] pub struct PathResolver { base: PathBuf, id: FileId, extension: String, } impl PathResolver { pub fn new(base: &Path, id: FileId, extension: String) -> Self { Self { base: base.to_owned(), id, extension, } } pub fn metadata_path_by_id(base: &Path, id: FileId) -> PathBuf { let mut path = base.to_path_buf(); path.push(PathBuf::from(id.clone())); path.set_extension("json"); path } pub fn id(&self) -> FileId { self.id.clone() } pub fn file_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); path.set_extension(self.extension.clone()); path } pub fn metadata_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); path.set_extension("json"); path } pub fn thumbnail_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(PathBuf::from(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 { 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| FileId::from(s))) .ok_or(PathError::InvalidPath)?, extension: path .extension() .and_then(|s| s.to_str().map(|s| s.to_owned())) .ok_or(PathError::InvalidPath)?, }) } } /// One file in the database, complete with the path of the file and information about the /// thumbnail of the file. #[derive(Debug)] pub struct FileHandle { pub id: FileId, pub path: PathResolver, pub info: FileInfo, } impl FileHandle { /// Create a new entry in the database pub fn new(filename: String, root: PathBuf) -> Result { let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); let extension = PathBuf::from(filename) .extension() .and_then(|s| s.to_str().map(|s| s.to_owned())) .ok_or(WriteFileError::InvalidPath)?; let path = PathResolver { base: root.clone(), id: id.clone(), extension: extension.clone(), }; let file_type = mime_guess::from_ext(&extension) .first_or_text_plain() .essence_str() .to_owned(); let info = FileInfo { id: id.clone(), size: 0, created: Utc::now(), file_type, hash: "".to_owned(), extension, }; let mut md_file = std::fs::File::create(path.metadata_path())?; md_file.write(&serde_json::to_vec(&info)?)?; Ok(Self { id, path, info }) } pub fn load(id: &FileId, root: &Path) -> Result { let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?; let resolver = PathResolver::new(root, id.clone(), info.extension.clone()); Ok(Self { id: info.id.clone(), path: resolver, info, }) } pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { let mut content_file = std::fs::File::create(self.path.file_path())?; let byte_count = content_file.write(&content)?; self.info.size = byte_count; self.info.hash = self.hash_content(&content).as_string(); let mut md_file = std::fs::File::create(self.path.metadata_path())?; md_file.write(&serde_json::to_vec(&self.info)?)?; self.write_thumbnail()?; Ok(()) } pub fn content(&self) -> Result, ReadFileError> { load_content(&self.path.file_path()) } pub fn thumbnail(&self) -> Result, ReadFileError> { load_content(&self.path.thumbnail_path()) } fn hash_content(&self, data: &Vec) -> HexString { HexString::from_bytes(&Sha256::digest(data).to_vec()) } fn write_thumbnail(&self) -> Result<(), WriteFileError> { let img = image::open(&self.path.file_path())?; let tn = img.resize(640, 640, FilterType::Nearest); tn.save(&self.path.thumbnail_path())?; Ok(()) } pub fn delete(self) { let _ = std::fs::remove_file(self.path.thumbnail_path()); let _ = std::fs::remove_file(self.path.file_path()); let _ = std::fs::remove_file(self.path.metadata_path()); } } fn load_content(path: &Path) -> Result, ReadFileError> { let mut buf = Vec::new(); let mut file = std::fs::File::open(&path)?; file.read_to_end(&mut buf)?; Ok(buf) } #[cfg(test)] mod test { use super::*; use std::{convert::TryFrom, path::PathBuf}; use tempdir::TempDir; #[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 it_opens_a_file() { let tmp = TempDir::new("var").unwrap(); FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); } #[test] fn it_deletes_a_file() { let tmp = TempDir::new("var").unwrap(); let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); f.delete(); } #[test] fn it_can_return_a_thumbnail() { let tmp = TempDir::new("var").unwrap(); let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); /* assert_eq!( f.thumbnail(), Thumbnail { id: String::from("rawr.png"), root: PathBuf::from("var/"), }, ); */ } #[test] fn it_can_return_a_file_stream() { let tmp = TempDir::new("var").unwrap(); let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { let tmp = TempDir::new("var").unwrap(); match FileHandle::load(&FileId::from("rawr"), tmp.path()) { Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), } } }