Provide a unified interface for the File and Thumbnail

This commit is contained in:
Savanni D'Gerinel 2023-09-23 20:20:35 -04:00
parent 89594d3169
commit 8afbe1ddc1
5 changed files with 135 additions and 82 deletions

View File

@ -30,7 +30,7 @@ mod middleware;
mod pages; mod pages;
mod store; mod store;
pub use store::{File, FileId, FileInfo, Store}; pub use store::{File, FileId, FileInfo, HasContent, Store};
/* /*
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
@ -229,7 +229,7 @@ fn script(_: &mut Request) -> IronResult<Response> {
fn serve_file( fn serve_file(
info: FileInfo, info: FileInfo,
file: File, file: impl HasContent,
old_etags: Option<String>, old_etags: Option<String>,
) -> http::Result<http::Response<Vec<u8>>> { ) -> http::Result<http::Response<Vec<u8>>> {
match old_etags { match old_etags {
@ -243,7 +243,7 @@ fn serve_file(
.header("etag", info.hash) .header("etag", info.hash)
.status(StatusCode::OK) .status(StatusCode::OK)
.body(content), .body(content),
Err(err) => warp::http::Response::builder() Err(_) => warp::http::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(vec![]), .body(vec![]),
}, },
@ -360,7 +360,7 @@ pub async fn main() {
move |id: String, old_etags: Option<String>| match app move |id: String, old_etags: Option<String>| match app
.read() .read()
.unwrap() .unwrap()
.get_thumbnail(FileId::from(id)) .get_thumbnail(&FileId::from(id))
{ {
Ok((info, file)) => serve_file(info, file, old_etags), Ok((info, file)) => serve_file(info, file, old_etags),
Err(_err) => warp::http::Response::builder() Err(_err) => warp::http::Response::builder()
@ -377,7 +377,7 @@ pub async fn main() {
move |id: String, old_etags: Option<String>| match app move |id: String, old_etags: Option<String>| match app
.read() .read()
.unwrap() .unwrap()
.get_file(FileId::from(id)) .get_file(&FileId::from(id))
{ {
Ok((info, file)) => serve_file(info, file, old_etags), Ok((info, file)) => serve_file(info, file, old_etags),
Err(_err) => warp::http::Response::builder() Err(_err) => warp::http::Response::builder()

View File

@ -1,6 +1,6 @@
use super::{ use super::{
fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, PathResolver, ReadFileError, fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, HasContent, PathResolver,
WriteFileError, ReadFileError, WriteFileError,
}; };
use chrono::prelude::*; use chrono::prelude::*;
use hex_string::HexString; use hex_string::HexString;
@ -80,10 +80,6 @@ impl File {
Ok(()) Ok(())
} }
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
unimplemented!()
}
pub fn hash_content(&self) -> Result<HexString, ReadFileError> { pub fn hash_content(&self) -> Result<HexString, ReadFileError> {
let mut buf = Vec::new(); let mut buf = Vec::new();
let mut file = std::fs::File::open(self.path.file_path())?; let mut file = std::fs::File::open(self.path.file_path())?;
@ -190,6 +186,17 @@ impl File {
*/ */
} }
impl HasContent for File {
fn content(&self) -> Result<Vec<u8>, ReadFileError> {
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(self.path.file_path())?;
file.read_to_end(&mut content)?;
Ok(content)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -209,26 +216,19 @@ mod test {
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json"));
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
File::new( File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))).expect("to succeed");
"rawr.png".to_owned(),
FileContext(PathBuf::from("fixtures/")),
)
.expect("to succeed");
} }
#[test] #[test]
fn it_can_return_a_thumbnail() { fn it_can_return_a_thumbnail() {
let f = File::new( let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/")))
"rawr.png".to_owned(),
FileContext(PathBuf::from("fixtures/")),
)
.expect("to succeed"); .expect("to succeed");
/* /*
assert_eq!( assert_eq!(
f.thumbnail(), f.thumbnail(),
Thumbnail { Thumbnail {
id: String::from("rawr.png"), id: String::from("rawr.png"),
root: PathBuf::from("fixtures/"), root: PathBuf::from("var/"),
}, },
); );
*/ */
@ -236,17 +236,14 @@ mod test {
#[test] #[test]
fn it_can_return_a_file_stream() { fn it_can_return_a_file_stream() {
let f = File::new( let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/")))
"rawr.png".to_owned(),
FileContext(PathBuf::from("fixtures/")),
)
.expect("to succeed"); .expect("to succeed");
// f.stream().expect("to succeed"); // f.stream().expect("to succeed");
} }
#[test] #[test]
fn it_raises_an_error_when_file_not_found() { fn it_raises_an_error_when_file_not_found() {
let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path"); let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path");
match File::load(resolver) { match File::load(resolver) {
Err(ReadFileError::FileNotFound(_)) => assert!(true), Err(ReadFileError::FileNotFound(_)) => assert!(true),
_ => assert!(false), _ => assert!(false),

View File

@ -106,7 +106,7 @@ mod test {
#[test] #[test]
fn it_saves_and_loads_metadata() { fn it_saves_and_loads_metadata() {
let resolver = PathResolver::try_from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607.png") let resolver = PathResolver::try_from("var/1617654d-a588-4714-b4fa-e00ed0a8a607.png")
.expect("a valid path"); .expect("a valid path");
let _cleanup = FileCleanup(resolver.metadata_path()); let _cleanup = FileCleanup(resolver.metadata_path());

View File

@ -64,6 +64,10 @@ pub enum PathError {
InvalidPath, InvalidPath,
} }
pub trait HasContent {
fn content(&self) -> Result<Vec<u8>, ReadFileError>;
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PathResolver { pub struct PathResolver {
base: PathBuf, base: PathBuf,
@ -159,6 +163,19 @@ impl From<&str> for FileId {
} }
} }
impl From<FileId> 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 { impl Deref for FileId {
type Target = String; type Target = String;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -198,7 +215,7 @@ impl Store {
Ok(file) Ok(file)
} }
pub fn delete_file(&mut self, id: FileId) -> Result<(), WriteFileError> { pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> {
/* /*
let f = File::open(&id, &self.files_root)?; let f = File::open(&id, &self.files_root)?;
f.delete() f.delete()
@ -206,19 +223,19 @@ impl Store {
unimplemented!() unimplemented!()
} }
pub fn get_metadata(&self, id: FileId) -> Result<FileInfo, ReadFileError> { pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
let mut path = self.files_root.clone(); let mut path = self.files_root.clone();
path.push(PathBuf::from((*id).clone())); path.push(PathBuf::from(id));
path.set_extension("json"); path.set_extension("json");
FileInfo::load(path) FileInfo::load(path)
} }
pub fn get_file(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { pub fn get_file(&self, id: &FileId) -> Result<(FileInfo, File), ReadFileError> {
let info = self.get_metadata(id.clone())?; let info = self.get_metadata(id)?;
let resolver = PathResolver { let resolver = PathResolver {
base: self.files_root.clone(), base: self.files_root.clone(),
id: (*id).clone(), id: (**id).clone(),
extension: info.extension.clone(), extension: info.extension.clone(),
}; };
@ -226,8 +243,17 @@ impl Store {
Ok((info, f)) Ok((info, f))
} }
pub fn get_thumbnail(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { pub fn get_thumbnail(&self, id: &FileId) -> Result<(FileInfo, Thumbnail), ReadFileError> {
unimplemented!() 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))
} }
} }
@ -238,6 +264,24 @@ mod test {
use cool_asserts::assert_matches; use cool_asserts::assert_matches;
use std::io::Read; use std::io::Read;
fn with_file<F>(test_fn: F)
where
F: FnOnce(Store, FileId),
{
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.png", *file_record.id)));
test_fn(store, file_record.id);
}
#[test] #[test]
fn paths() { fn paths() {
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
@ -258,42 +302,21 @@ mod test {
#[test] #[test]
fn adds_files() { fn adds_files() {
let mut buf = Vec::new(); with_file(|store, id| {
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); let (_, file) = store.get_file(&id).expect("to retrieve the file");
file.read_to_end(&mut buf).unwrap();
let mut store = Store::new(PathBuf::from("var/")); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); assert!(PathBuf::from(format!("var/{}.png", *id)).exists());
let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); assert!(PathBuf::from(format!("var/{}.json", *id)).exists());
let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); assert!(PathBuf::from(format!("var/{}.tn.png", *id)).exists());
});
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] #[test]
fn sets_up_metadata_for_file() { fn sets_up_metadata_for_file() {
let mut buf = Vec::new(); with_file(|store, id| {
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); let info = store.get_metadata(&id).expect("to retrieve the metadata");
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_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
assert_eq!(size, 23777); assert_eq!(size, 23777);
@ -301,17 +324,27 @@ mod test {
assert_eq!(hash, "".to_owned()); assert_eq!(hash, "".to_owned());
assert_eq!(extension, "png".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] #[test]
fn sets_up_thumbnail_for_file() {} 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] #[test]
fn deletes_associated_files() {} fn deletes_associated_files() {
with_file(|mut store, id| {
store.delete_file(&id).expect("file to be deleted");
assert!(!PathBuf::from(format!("var/{}.png", *id)).exists());
assert!(!PathBuf::from(format!("var/{}.json", *id)).exists());
assert!(!PathBuf::from(format!("var/{}.tn.png", *id)).exists());
});
}
#[test] #[test]
fn lists_files_in_the_db() {} fn lists_files_in_the_db() {}

View File

@ -1,8 +1,10 @@
use super::{HasContent, ReadFileError, WriteFileError};
use image::imageops::FilterType; use image::imageops::FilterType;
use std::fs::remove_file; use std::{
use std::path::{Path, PathBuf}; fs::remove_file,
io::Read,
use super::{ReadFileError, WriteFileError}; path::{Path, PathBuf},
};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Thumbnail { pub struct Thumbnail {
@ -27,6 +29,16 @@ impl Thumbnail {
Ok(s) Ok(s)
} }
pub fn load(path: PathBuf) -> Result<Thumbnail, ReadFileError> {
let s = Thumbnail { path: path.clone() };
if !s.path.exists() {
return Err(ReadFileError::FileNotFound(path));
}
Ok(s)
}
/* /*
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> { pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
let id = path let id = path
@ -61,6 +73,17 @@ impl Thumbnail {
*/ */
} }
impl HasContent for Thumbnail {
fn content(&self) -> Result<Vec<u8>, ReadFileError> {
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(&self.path)?;
file.read_to_end(&mut content)?;
Ok(content)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;