Compare commits

...

5 Commits

10 changed files with 109 additions and 140 deletions

1
Cargo.lock generated
View File

@ -692,6 +692,7 @@ dependencies = [
"serde 1.0.188", "serde 1.0.188",
"serde_json", "serde_json",
"sha2", "sha2",
"tempdir",
"thiserror", "thiserror",
"tokio", "tokio",
"uuid 0.4.0", "uuid 0.4.0",

View File

@ -32,4 +32,4 @@ log = { version = "0.4" }
bytes = { version = "1" } bytes = { version = "1" }
futures-util = { version = "0.3" } futures-util = { version = "0.3" }
cool_asserts = { version = "2" } cool_asserts = { version = "2" }
tempdir = { version = "0.3" }

View File

@ -1 +0,0 @@
{"id":"rawr.png","size":23777,"created":"2020-06-04T16:04:10.085680927Z","file_type":"image/png","hash":"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,11 +1,3 @@
/*
use iron::headers;
use iron::middleware::Handler;
use iron::modifiers::{Header, Redirect};
use iron::prelude::*;
use iron::response::BodyReader;
use iron::status;
*/
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -14,9 +6,8 @@ use http::status::StatusCode;
// use orizentic::{Permissions, ResourceName, Secret}; // use orizentic::{Permissions, ResourceName, Secret};
use build_html::Html; use build_html::Html;
use bytes::Buf; use bytes::Buf;
use futures_util::{Stream, StreamExt}; use futures_util::StreamExt;
use std::{ use std::{
collections::HashMap,
io::Read, io::Read,
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf, path::PathBuf,
@ -24,9 +15,9 @@ use std::{
}; };
use warp::{filters::multipart::Part, Filter}; use warp::{filters::multipart::Part, Filter};
mod cookies; // mod cookies;
mod html; mod html;
mod middleware; // mod middleware;
mod pages; mod pages;
mod store; mod store;
@ -430,7 +421,7 @@ pub async fn main() {
.with(log), .with(log),
); );
*/ */
let server = warp::serve(root.or(upload).with(log)); let server = warp::serve(root.or(upload).or(thumbnail).or(file).or(delete).with(log));
server server
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
.await; .await;

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
html::*, html::*,
store::{FileHandle, FileId, ReadFileError, Thumbnail}, store::{FileHandle, FileId, ReadFileError},
}; };
use build_html::{self, Container, ContainerType, Html, HtmlContainer}; use build_html::{self, Container, ContainerType, Html, HtmlContainer};

View File

@ -21,27 +21,34 @@ pub enum PathError {
pub struct PathResolver { pub struct PathResolver {
base: PathBuf, base: PathBuf,
id: FileId, id: FileId,
extension: String,
} }
impl PathResolver { impl PathResolver {
pub fn new(base: &Path, id: FileId) -> Self { pub fn new(base: &Path, id: FileId, extension: String) -> Self {
Self { Self {
base: base.to_owned(), base: base.to_owned(),
id, 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 { pub fn id(&self) -> FileId {
self.id.clone() self.id.clone()
} }
pub fn file_path(&self) -> Result<PathBuf, ReadFileError> { pub fn file_path(&self) -> PathBuf {
let info = FileInfo::load(self.metadata_path())?;
let mut path = self.base.clone(); let mut path = self.base.clone();
path.push(PathBuf::from(self.id.clone())); path.push(PathBuf::from(self.id.clone()));
path.set_extension(info.extension); path.set_extension(self.extension.clone());
Ok(path) path
} }
pub fn metadata_path(&self) -> PathBuf { pub fn metadata_path(&self) -> PathBuf {
@ -51,13 +58,11 @@ impl PathResolver {
path path
} }
pub fn thumbnail_path(&self) -> Result<PathBuf, ReadFileError> { pub fn thumbnail_path(&self) -> PathBuf {
let info = FileInfo::load(self.metadata_path())?;
let mut path = self.base.clone(); let mut path = self.base.clone();
path.push(PathBuf::from(self.id.clone())); path.push(PathBuf::from(self.id.clone()));
path.set_extension(format!("tn.{}", info.extension)); path.set_extension(format!("tn.{}", self.extension));
Ok(path) path
} }
} }
@ -71,7 +76,6 @@ impl TryFrom<String> for PathResolver {
impl TryFrom<&str> for PathResolver { impl TryFrom<&str> for PathResolver {
type Error = PathError; type Error = PathError;
fn try_from(s: &str) -> Result<Self, Self::Error> { fn try_from(s: &str) -> Result<Self, Self::Error> {
let path = Path::new(s);
PathResolver::try_from(Path::new(s)) PathResolver::try_from(Path::new(s))
} }
} }
@ -95,6 +99,10 @@ impl TryFrom<&Path> for PathResolver {
.file_stem() .file_stem()
.and_then(|s| s.to_str().map(|s| FileId::from(s))) .and_then(|s| s.to_str().map(|s| FileId::from(s)))
.ok_or(PathError::InvalidPath)?, .ok_or(PathError::InvalidPath)?,
extension: path
.extension()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(PathError::InvalidPath)?,
}) })
} }
} }
@ -120,6 +128,7 @@ impl FileHandle {
let path = PathResolver { let path = PathResolver {
base: root.clone(), base: root.clone(),
id: id.clone(), id: id.clone(),
extension: extension.clone(),
}; };
let file_type = mime_guess::from_ext(&extension) let file_type = mime_guess::from_ext(&extension)
@ -143,8 +152,8 @@ impl FileHandle {
} }
pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> { pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> {
let resolver = PathResolver::new(root, id.clone()); let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?;
let info = FileInfo::load(resolver.metadata_path())?; let resolver = PathResolver::new(root, id.clone(), info.extension.clone());
Ok(Self { Ok(Self {
id: info.id.clone(), id: info.id.clone(),
path: resolver, path: resolver,
@ -153,69 +162,42 @@ impl FileHandle {
} }
pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> { pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> {
let mut content_file = std::fs::File::create( let mut content_file = std::fs::File::create(self.path.file_path())?;
self.path
.file_path()
.map_err(|_| WriteFileError::NoMetadata)?,
)?;
let byte_count = content_file.write(&content)?; let byte_count = content_file.write(&content)?;
self.info.size = byte_count; 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())?; let mut md_file = std::fs::File::create(self.path.metadata_path())?;
md_file.write(&serde_json::to_vec(&self.info)?)?; md_file.write(&serde_json::to_vec(&self.info)?)?;
self.write_thumbnail(); self.write_thumbnail()?;
Ok(()) Ok(())
} }
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> { pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
load_content(&self.path.file_path()?) load_content(&self.path.file_path())
} }
pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> { pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> {
load_content(&self.path.thumbnail_path()?) load_content(&self.path.thumbnail_path())
} }
fn hash_content(&self) -> Result<HexString, ReadFileError> { fn hash_content(&self, data: &Vec<u8>) -> HexString {
let mut buf = Vec::new(); HexString::from_bytes(&Sha256::digest(data).to_vec())
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> { fn write_thumbnail(&self) -> Result<(), WriteFileError> {
let img = image::open( let img = image::open(&self.path.file_path())?;
&self
.path
.file_path()
.map_err(|_| WriteFileError::NoMetadata)?,
)?;
let tn = img.resize(640, 640, FilterType::Nearest); let tn = img.resize(640, 640, FilterType::Nearest);
tn.save( tn.save(&self.path.thumbnail_path())?;
&self
.path
.thumbnail_path()
.map_err(|_| WriteFileError::NoMetadata)?,
)?;
Ok(()) Ok(())
} }
pub fn delete(self) -> Result<(), WriteFileError> { pub fn delete(self) {
std::fs::remove_file( let _ = std::fs::remove_file(self.path.thumbnail_path());
self.path let _ = std::fs::remove_file(self.path.file_path());
.thumbnail_path() let _ = std::fs::remove_file(self.path.metadata_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(())
} }
} }
@ -229,43 +211,47 @@ fn load_content(path: &Path) -> Result<Vec<u8>, ReadFileError> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::store::utils::FileCleanup;
use cool_asserts::assert_matches;
use std::{convert::TryFrom, path::PathBuf}; use std::{convert::TryFrom, path::PathBuf};
use tempdir::TempDir;
#[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")
.expect("to have a valid path"); .expect("to have a valid path");
assert_matches!(
assert_eq!(
resolver.file_path(), resolver.file_path(),
Ok(path) => assert_eq!(path, PathBuf::from( PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
"path/82420255-d3c8-4d90-a582-f94be588c70c.png"
))
); );
assert_eq!( assert_eq!(
resolver.metadata_path(), resolver.metadata_path(),
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json")
); );
assert_matches!( assert_eq!(
resolver.thumbnail_path(), resolver.thumbnail_path(),
Ok(path) => assert_eq!(path, PathBuf::from( PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png")
"path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png"
))
); );
} }
#[test] #[test]
fn it_opens_a_file() { fn it_opens_a_file() {
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); let tmp = TempDir::new("var").unwrap();
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
}
FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).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] #[test]
fn it_can_return_a_thumbnail() { fn it_can_return_a_thumbnail() {
let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); let tmp = TempDir::new("var").unwrap();
let _ =
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
/* /*
assert_eq!( assert_eq!(
f.thumbnail(), f.thumbnail(),
@ -279,14 +265,16 @@ mod test {
#[test] #[test]
fn it_can_return_a_file_stream() { fn it_can_return_a_file_stream() {
let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); 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"); // 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("var/rawr.png").expect("a valid path"); let tmp = TempDir::new("var").unwrap();
match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) { match FileHandle::load(&FileId::from("rawr"), tmp.path()) {
Err(ReadFileError::FileNotFound(_)) => assert!(true), Err(ReadFileError::FileNotFound(_)) => assert!(true),
_ => assert!(false), _ => assert!(false),
} }

View File

@ -41,15 +41,12 @@ impl FileInfo {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId}; use crate::store::FileId;
use std::convert::TryFrom; use tempdir::TempDir;
#[test] #[test]
fn it_saves_and_loads_metadata() { fn it_saves_and_loads_metadata() {
let resolver = PathResolver::try_from("var/1617654d-a588-4714-b4fa-e00ed0a8a607.png") let tmp = TempDir::new("var").unwrap();
.expect("a valid path");
let _cleanup = FileCleanup(resolver.metadata_path());
let created = Utc::now(); let created = Utc::now();
let info = FileInfo { let info = FileInfo {
@ -60,9 +57,11 @@ mod test {
hash: "abcdefg".to_owned(), hash: "abcdefg".to_owned(),
extension: "png".to_owned(), extension: "png".to_owned(),
}; };
info.save(resolver.metadata_path()).unwrap(); let mut path = tmp.path().to_owned();
path.push(&PathBuf::from(info.id.clone()));
info.save(path.clone()).unwrap();
let info_ = FileInfo::load(resolver.metadata_path()).unwrap(); let info_ = FileInfo::load(path).unwrap();
assert_eq!(info_.size, 23777); assert_eq!(info_.size, 23777);
assert_eq!(info_.created, info.created); assert_eq!(info_.created, info.created);
assert_eq!(info_.file_type, "image/png"); assert_eq!(info_.file_type, "image/png");

View File

@ -1,18 +1,13 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::collections::HashSet;
ops::Deref, use std::{ops::Deref, path::PathBuf};
path::{Path, PathBuf},
};
use thiserror::Error; use thiserror::Error;
mod filehandle; mod filehandle;
mod fileinfo; mod fileinfo;
mod thumbnail;
pub mod utils;
pub use filehandle::FileHandle; pub use filehandle::FileHandle;
pub use fileinfo::FileInfo; pub use fileinfo::FileInfo;
pub use thumbnail::Thumbnail;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum WriteFileError { pub enum WriteFileError {
@ -118,8 +113,21 @@ impl Store {
Self { files_root } Self { files_root }
} }
pub fn list_files(&self) -> Result<Vec<FileId>, ReadFileError> { pub fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
unimplemented!() let paths = std::fs::read_dir(&self.files_root)?;
let info_files = paths
.into_iter()
.filter_map(|path| {
let path_ = path.unwrap().path();
if path_.extension().and_then(|s| s.to_str()) == Some("json") {
let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap();
Some(FileId::from(FileId::from(stem)))
} else {
None
}
})
.collect::<HashSet<FileId>>();
Ok(info_files)
} }
pub fn add_file( pub fn add_file(
@ -153,50 +161,49 @@ impl Store {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::store::utils::FileCleanup;
use cool_asserts::assert_matches; use cool_asserts::assert_matches;
use std::{collections::HashSet, io::Read}; use std::{collections::HashSet, io::Read};
use tempdir::TempDir;
fn with_file<F>(test_fn: F) fn with_file<F>(test_fn: F)
where where
F: FnOnce(Store, FileId), F: FnOnce(Store, FileId, TempDir),
{ {
let tmp = TempDir::new("var").unwrap();
let mut buf = Vec::new(); let mut buf = Vec::new();
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
file.read_to_end(&mut buf).unwrap(); file.read_to_end(&mut buf).unwrap();
let mut store = Store::new(PathBuf::from("var/")); let mut store = Store::new(PathBuf::from(tmp.path()));
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); test_fn(store, file_record.id, tmp);
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 adds_files() { fn adds_files() {
with_file(|store, id| { with_file(|store, id, tmp| {
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); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
assert!(PathBuf::from(format!("var/{}.png", *id)).exists()); assert!(tmp.path().join(&(*id)).with_extension("png").exists());
assert!(PathBuf::from(format!("var/{}.json", *id)).exists()); assert!(tmp.path().join(&(*id)).with_extension("json").exists());
assert!(PathBuf::from(format!("var/{}.tn.png", *id)).exists()); assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists());
}); });
} }
#[test] #[test]
fn sets_up_metadata_for_file() { fn sets_up_metadata_for_file() {
with_file(|store, id| { with_file(|store, id, tmp| {
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
let info = store.get_metadata(&id).expect("to retrieve the metadata"); let info = store.get_metadata(&id).expect("to retrieve the metadata");
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);
assert_eq!(file_type, "image/png"); assert_eq!(file_type, "image/png");
assert_eq!(hash, "".to_owned()); assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned());
assert_eq!(extension, "png".to_owned()); assert_eq!(extension, "png".to_owned());
}); });
}); });
@ -214,22 +221,23 @@ mod test {
#[test] #[test]
fn deletes_associated_files() { fn deletes_associated_files() {
with_file(|mut store, id| { with_file(|mut store, id, tmp| {
store.delete_file(&id).expect("file to be deleted"); store.delete_file(&id).expect("file to be deleted");
assert!(!PathBuf::from(format!("var/{}.png", *id)).exists()); assert!(!tmp.path().join(&(*id)).with_extension("png").exists());
assert!(!PathBuf::from(format!("var/{}.json", *id)).exists()); assert!(!tmp.path().join(&(*id)).with_extension("json").exists());
assert!(!PathBuf::from(format!("var/{}.tn.png", *id)).exists()); assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists());
}); });
} }
#[test] #[test]
fn lists_files_in_the_db() { fn lists_files_in_the_db() {
with_file(|store, id| { with_file(|store, id, _| {
let resolvers = store.list_files().expect("file listing to succeed"); let resolvers = store.list_files().expect("file listing to succeed");
let ids = resolvers.into_iter().collect::<HashSet<FileId>>(); let ids = resolvers.into_iter().collect::<HashSet<FileId>>();
assert_eq!(ids.len(), 1); assert_eq!(ids.len(), 1);
assert!(ids.contains(&id));
}); });
} }
} }

View File

@ -1,17 +0,0 @@
use std::path::{Path, PathBuf};
pub struct FileCleanup(pub PathBuf);
impl Drop for FileCleanup {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub fn append_extension(path: &Path, extra_ext: &str) -> PathBuf {
let ext_ = match path.extension() {
None => String::from(extra_ext),
Some(ext) => [ext.to_string_lossy(), std::borrow::Cow::from(extra_ext)].join("."),
};
path.with_extension(ext_)
}