Provide a unified interface for the File and Thumbnail
This commit is contained in:
parent
809861a38e
commit
98a07ce03e
|
@ -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()
|
||||||
|
|
|
@ -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(),
|
.expect("to succeed");
|
||||||
FileContext(PathBuf::from("fixtures/")),
|
|
||||||
)
|
|
||||||
.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(),
|
.expect("to succeed");
|
||||||
FileContext(PathBuf::from("fixtures/")),
|
|
||||||
)
|
|
||||||
.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),
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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,60 +302,49 @@ 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/"));
|
assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
|
||||||
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
assert_eq!(size, 23777);
|
||||||
|
assert_eq!(file_type, "image/png");
|
||||||
let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id)));
|
assert_eq!(hash, "".to_owned());
|
||||||
let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id)));
|
assert_eq!(extension, "png".to_owned());
|
||||||
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_eq!(size, 23777);
|
|
||||||
assert_eq!(file_type, "image/png");
|
|
||||||
assert_eq!(hash, "".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() {}
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
Loading…
Reference in New Issue