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, } impl PathResolver { pub fn new(base: &Path, id: FileId) -> Self { Self { base: base.to_owned(), id, } } pub fn id(&self) -> FileId { self.id.clone() } pub fn file_path(&self) -> Result { let info = FileInfo::load(self.metadata_path())?; let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); path.set_extension(info.extension); Ok(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) -> Result { let info = FileInfo::load(self.metadata_path())?; let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); path.set_extension(format!("tn.{}", info.extension)); Ok(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| FileId::from(s))) .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(), }; 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 resolver = PathResolver::new(root, id.clone()); let info = FileInfo::load(resolver.metadata_path())?; 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() .map_err(|_| WriteFileError::NoMetadata)?, )?; let byte_count = content_file.write(&content)?; self.info.size = byte_count; 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) -> Result { let mut buf = Vec::new(); let mut file = std::fs::File::open(self.path.file_path()?)?; file.read_to_end(&mut buf).map_err(ReadFileError::from)?; Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec())) } fn write_thumbnail(&self) -> Result<(), WriteFileError> { let img = image::open( &self .path .file_path() .map_err(|_| WriteFileError::NoMetadata)?, )?; let tn = img.resize(640, 640, FilterType::Nearest); tn.save( &self .path .thumbnail_path() .map_err(|_| WriteFileError::NoMetadata)?, )?; Ok(()) } pub fn delete(self) -> Result<(), WriteFileError> { std::fs::remove_file( self.path .thumbnail_path() .map_err(|_| WriteFileError::NoMetadata)?, )?; std::fs::remove_file(self.path.metadata_path())?; std::fs::remove_file( self.path .file_path() .map_err(|_| WriteFileError::NoMetadata)?, )?; Ok(()) } } 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 crate::store::utils::FileCleanup; use cool_asserts::assert_matches; use std::{convert::TryFrom, path::PathBuf}; #[test] fn paths() { let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") .expect("to have a valid path"); assert_matches!( resolver.file_path(), Ok(path) => assert_eq!(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_matches!( resolver.thumbnail_path(), Ok(path) => assert_eq!(path, PathBuf::from( "path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png" )) ); } #[test] fn it_opens_a_file() { let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); } #[test] fn it_can_return_a_thumbnail() { let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).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 f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path"); match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) { Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), } } }