From 756120c9e6549928a54ade190679c727bb211dff Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Sep 2023 12:08:09 -0400 Subject: [PATCH] Clean up the filehandle logic --- file-service/src/main.rs | 40 ++-- file-service/src/pages.rs | 18 +- file-service/src/store/file.rs | 252 ----------------------- file-service/src/store/filehandle.rs | 294 +++++++++++++++++++++++++++ file-service/src/store/fileinfo.rs | 62 +----- file-service/src/store/mod.rs | 186 ++++------------- file-service/src/store/thumbnail.rs | 13 +- 7 files changed, 366 insertions(+), 499 deletions(-) delete mode 100644 file-service/src/store/file.rs create mode 100644 file-service/src/store/filehandle.rs diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 5c3575f..549854e 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -30,7 +30,7 @@ mod middleware; mod pages; mod store; -pub use store::{File, FileId, FileInfo, HasContent, Store}; +pub use store::{FileHandle, FileId, FileInfo, ReadFileError, Store}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -228,19 +228,18 @@ fn script(_: &mut Request) -> IronResult { */ fn serve_file( - info: FileInfo, - file: impl HasContent, + file: FileHandle, old_etags: Option, ) -> http::Result>> { match old_etags { - Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() - .header("content-type", info.file_type) + Some(old_etags) if old_etags != file.info.hash => warp::http::Response::builder() + .header("content-type", file.info.file_type) .status(StatusCode::NOT_MODIFIED) .body(vec![]), _ => match file.content() { Ok(content) => warp::http::Response::builder() - .header("content-type", info.file_type) - .header("etag", info.hash) + .header("content-type", file.info.file_type) + .header("etag", file.info.hash) .status(StatusCode::OK) .body(content), Err(_) => warp::http::Response::builder() @@ -331,10 +330,23 @@ pub async fn main() { let app = app.clone(); move || { info!("root handler"); - warp::http::Response::builder() - .header("content-type", "text/html") - .status(StatusCode::OK) - .body(pages::index(app.read().unwrap().list_files()).to_html_string()) + let app = app.read().unwrap(); + match app.list_files() { + Ok(ids) => { + let files = ids + .into_iter() + .map(|id| app.get_file(&id)) + .collect::>>(); + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::OK) + .body(pages::index(files).to_html_string()) + } + Err(_) => warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_owned()), + } } }); @@ -360,9 +372,9 @@ pub async fn main() { move |id: String, old_etags: Option| match app .read() .unwrap() - .get_thumbnail(&FileId::from(id)) + .get_file(&FileId::from(id)) { - Ok((info, file)) => serve_file(info, file, old_etags), + Ok(file) => serve_file(file, old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), @@ -379,7 +391,7 @@ pub async fn main() { .unwrap() .get_file(&FileId::from(id)) { - Ok((info, file)) => serve_file(info, file, old_etags), + Ok(file) => serve_file(file, old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 8fac465..a95e70a 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,10 +1,10 @@ use crate::{ html::*, - store::{File, ReadFileError}, + store::{FileHandle, FileId, ReadFileError, Thumbnail}, }; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; -pub fn index(files: Vec>) -> build_html::HtmlPage { +pub fn index(handles: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() .with_title("Admin list of files") .with_header(1, "Admin list of files") @@ -20,11 +20,11 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { .with_html(Button::new("Upload file")), ); - for file in files { - let container = match file { - Ok(ref file) => thumbnail(file).with_html( + for handle in handles { + let container = match handle { + Ok(ref handle) => thumbnail(&handle.id).with_html( Form::new() - .with_path(&format!("/{}", *file.id)) + .with_path(&format!("/{}", *handle.id)) .with_method("post") .with_html(Input::new("hidden", "_method").with_value("delete")) .with_html(Button::new("Delete")), @@ -39,13 +39,13 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { page } -pub fn thumbnail(file: &File) -> Container { +pub fn thumbnail(id: &FileId) -> Container { let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); let tn = Container::new(ContainerType::Div) .with_attributes(vec![("class", "thumbnail")]) .with_link( - format!("/{}", *file.id), - Image::new(&format!("{}/tn", *file.id)).to_html_string(), + format!("/{}", **id), + Image::new(&format!("{}/tn", **id)).to_html_string(), ); container.add_html(tn); container diff --git a/file-service/src/store/file.rs b/file-service/src/store/file.rs deleted file mode 100644 index 5449ec3..0000000 --- a/file-service/src/store/file.rs +++ /dev/null @@ -1,252 +0,0 @@ -use super::{ - fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, HasContent, PathResolver, - ReadFileError, WriteFileError, -}; -use chrono::prelude::*; -use hex_string::HexString; -use sha2::{Digest, Sha256}; -use std::{ - convert::TryFrom, - io::{Read, Write}, - path::{Path, PathBuf}, -}; -use uuid::Uuid; - -/// One file in the database, complete with the path of the file and information about the -/// thumbnail of the file. -#[derive(Debug)] -pub struct File { - pub id: FileId, - pub path: PathResolver, - pub info: FileInfo, -} - -impl File { - /// Create a new entry in the database - pub fn new(filename: String, context: CTX) -> Result { - let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); - - let mut path = context.root(); - let extension = PathBuf::from(filename) - .extension() - .and_then(|s| s.to_str().map(|s| s.to_owned())) - .ok_or(WriteFileError::InvalidPath)?; - path.push((*id).clone()); - let path = PathResolver { - base: context.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(resolver: PathResolver) -> Result { - 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())?; - 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)?)?; - - Thumbnail::open(self.path.file_path(), self.path.thumbnail_path())?; - - Ok(()) - } - - pub 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())) - } - - /* - pub fn new( - id: &str, - root: &Path, - filename: &Option, - ) -> Result { - let mut dest_path = PathBuf::from(root); - dest_path.push(id); - match filename { - Some(fname) => match fname.extension() { - Some(ext) => { - dest_path.set_extension(ext); - () - } - None => (), - }, - None => (), - }; - copy(temp_path, dest_path.clone())?; - let info = FileInfo::from_path(&dest_path)?; - let tn = Thumbnail::from_path(&dest_path)?; - Ok(Self { - info, - tn, - root: PathBuf::from(root), - }) - } - - pub fn open(id: &str, root: &Path) -> Result { - let mut file_path = PathBuf::from(root); - file_path.push(id.clone()); - - if !file_path.exists() { - return Err(FileError::FileNotFound(file_path)); - } - if !file_path.is_file() { - return Err(FileError::NotAFile(file_path)); - } - - let info = match FileInfo::open(id, root) { - Ok(i) => Ok(i), - Err(FileError::FileNotFound(_)) => { - let info = FileInfo::from_path(&file_path)?; - info.save(&root)?; - Ok(info) - } - Err(err) => Err(err), - }?; - - let tn = Thumbnail::open(id, root)?; - - Ok(Self { - info, - tn, - root: PathBuf::from(root), - }) - } - - pub fn list(root: &Path) -> Vec> { - let dir_iter = read_dir(&root).unwrap(); - dir_iter - .filter(|entry| { - let entry_ = entry.as_ref().unwrap(); - let filename = entry_.file_name(); - !(filename.to_string_lossy().starts_with(".")) - }) - .map(|entry| { - let entry_ = entry.unwrap(); - let id = entry_.file_name().into_string().unwrap(); - Self::open(&id, root) - }) - .collect() - } - - pub fn info(&self) -> FileInfo { - self.info.clone() - } - - pub fn thumbnail(&self) -> Thumbnail { - self.tn.clone() - } - - pub fn stream(&self) -> Result { - let mut path = self.root.clone(); - path.push(self.info.id.clone()); - std::fs::File::open(path).map_err(FileError::from) - } - - pub fn delete(&self) -> Result<(), FileError> { - let mut path = self.root.clone(); - path.push(self.info.id.clone()); - remove_file(path)?; - self.tn.delete()?; - self.info.delete() - } - */ -} - -impl HasContent for File { - fn content(&self) -> Result, ReadFileError> { - let mut content: Vec = Vec::new(); - - let mut file = std::fs::File::open(self.path.file_path())?; - file.read_to_end(&mut content)?; - - Ok(content) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::store::utils::FileCleanup; - use std::{convert::TryFrom, path::PathBuf}; - - struct FileContext(PathBuf); - - impl FileRoot for FileContext { - fn root(&self) -> PathBuf { - self.0.clone() - } - } - - #[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")); - - File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))).expect("to succeed"); - } - - #[test] - fn it_can_return_a_thumbnail() { - let f = File::new("rawr.png".to_owned(), FileContext(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 = File::new("rawr.png".to_owned(), FileContext(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 File::load(resolver) { - Err(ReadFileError::FileNotFound(_)) => assert!(true), - _ => assert!(false), - } - } -} diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs new file mode 100644 index 0000000..50f79d5 --- /dev/null +++ b/file-service/src/store/filehandle.rs @@ -0,0 +1,294 @@ +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), + } + } +} diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index 55223a4..6f253ff 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -36,72 +36,12 @@ impl FileInfo { file.write(ser.as_bytes())?; Ok(()) } - - /* - pub fn from_path(path: &Path) -> Result { - match (path.is_file(), path.is_dir()) { - (false, false) => Err(ReadFileError::FileNotFound), - (false, true) => Err(ReadFileError::NotAFile), - (true, _) => Ok(()), - }?; - - let metadata = path.metadata().map_err(ReadFileError::IOError)?; - let id = path - .file_name() - .map(|s| String::from(s.to_string_lossy())) - .ok_or(ReadFileError::NotAFile)?; - let created = metadata - .created() - .map(|m| DateTime::from(m)) - .map_err(|err| ReadFileError::IOError(err))?; - let file_type = String::from( - mime_guess::from_path(path) - .first_or_octet_stream() - .essence_str(), - ); - let hash = FileInfo::hash_file(path)?; - Ok(FileInfo { - id, - size: metadata.len(), - created, - file_type, - hash: hash.as_string(), - root: PathBuf::from(path.parent().unwrap()), - }) - } - */ - - /* - fn hash_file(path: &Path) -> Result { - let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?; - - file.read_to_end(&mut buf).map_err(ReadFileError::from)?; - let mut vec = Vec::new(); - vec.extend_from_slice(Sha256::digest(&buf).as_slice()); - Ok(HexString::from_bytes(&vec)) - } - */ - - /* - fn metadata_path(id: &str, root: &Path) -> PathBuf { - let mut path = PathBuf::from(root); - path.push(".metadata"); - path.push(id.clone()); - append_extension(&path, "json") - } - - pub fn delete(&self) -> Result<(), FileError> { - let path = FileInfo::metadata_path(&self.id, &self.root); - remove_file(path).map_err(FileError::from) - } - */ } #[cfg(test)] mod test { use super::*; - use crate::store::{utils::FileCleanup, FileId, PathResolver}; + use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId}; use std::convert::TryFrom; #[test] diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 4d098e3..4a9ff6a 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,18 +1,16 @@ use serde::{Deserialize, Serialize}; use std::{ - convert::TryFrom, ops::Deref, path::{Path, PathBuf}, }; use thiserror::Error; -use uuid::Uuid; -mod file; +mod filehandle; mod fileinfo; mod thumbnail; pub mod utils; -pub use file::File; +pub use filehandle::FileHandle; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; @@ -27,6 +25,12 @@ pub enum WriteFileError { #[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), @@ -58,97 +62,7 @@ pub enum ReadFileError { IOError(#[from] std::io::Error), } -#[derive(Debug, Error)] -pub enum PathError { - #[error("path cannot be derived from input")] - InvalidPath, -} - -pub trait HasContent { - fn content(&self) -> Result, ReadFileError>; -} - -#[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)] +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct FileId(String); impl From for FileId { @@ -204,23 +118,28 @@ impl Store { Self { files_root } } - pub fn list_files(&self) -> Vec> { + pub fn list_files(&self) -> Result, ReadFileError> { 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)?; + 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<(), WriteFileError> { - /* - let f = File::open(&id, &self.files_root)?; - f.delete() - */ - unimplemented!() + let handle = FileHandle::load(id, &self.files_root)?; + handle.delete(); + Ok(()) } pub fn get_metadata(&self, id: &FileId) -> Result { @@ -229,32 +148,6 @@ impl Store { path.set_extension("json"); FileInfo::load(path) } - - pub fn get_file(&self, id: &FileId) -> Result<(FileInfo, File), ReadFileError> { - let info = self.get_metadata(id)?; - - 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, Thumbnail), ReadFileError> { - let info = self.get_metadata(id)?; - - let resolver = PathResolver { - base: self.files_root.clone(), - id: (**id).clone(), - extension: info.extension.clone(), - }; - - let f = Thumbnail::load(resolver.thumbnail_path())?; - Ok((info, f)) - } } #[cfg(test)] @@ -262,7 +155,7 @@ mod test { use super::*; use crate::store::utils::FileCleanup; use cool_asserts::assert_matches; - use std::io::Read; + use std::{collections::HashSet, io::Read}; fn with_file(test_fn: F) where @@ -282,28 +175,10 @@ mod test { test_fn(store, file_record.id); } - #[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() { with_file(|store, id| { - let (_, file) = store.get_file(&id).expect("to retrieve the file"); + let file = store.get_file(&id).expect("to retrieve the file"); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); @@ -327,6 +202,7 @@ mod test { }); } + /* #[test] fn sets_up_thumbnail_for_file() { with_file(|store, id| { @@ -334,6 +210,7 @@ mod test { assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869); }); } + */ #[test] fn deletes_associated_files() { @@ -347,5 +224,12 @@ mod test { } #[test] - fn lists_files_in_the_db() {} + 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); + }); + } } diff --git a/file-service/src/store/thumbnail.rs b/file-service/src/store/thumbnail.rs index 1c706cc..52c531f 100644 --- a/file-service/src/store/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -1,4 +1,4 @@ -use super::{HasContent, ReadFileError, WriteFileError}; +use super::{ReadFileError, WriteFileError}; use image::imageops::FilterType; use std::{ fs::remove_file, @@ -73,17 +73,6 @@ impl Thumbnail { */ } -impl HasContent for Thumbnail { - fn content(&self) -> Result, ReadFileError> { - let mut content: Vec = Vec::new(); - - let mut file = std::fs::File::open(&self.path)?; - file.read_to_end(&mut content)?; - - Ok(content) - } -} - #[cfg(test)] mod test { use super::*;