diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index d1e2f81..0afba1a 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -7,8 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +build_html = { version = "2" } chrono = { version = "0.4", features = ["serde"] } hex-string = "0.1.0" +http = { version = "0.2" } image = "0.23.5" iron = "0.6.1" logger = "*" @@ -22,4 +24,6 @@ serde_json = "*" serde = { version = "1.0", features = ["derive"] } sha2 = "0.8.2" thiserror = "1.0.20" -uuid = { version = "0.4", features = ["serde", "v4"] } +tokio = { version = "1", features = [ "full" ] } +uuid = { version = "0.4", features = [ "serde", "v4" ] } +warp = { version = "0.3" } diff --git a/file-service/fixtures/.metadata/rawr.png.json b/file-service/fixtures/.metadata/rawr.png.json new file mode 100644 index 0000000..0c6259a --- /dev/null +++ b/file-service/fixtures/.metadata/rawr.png.json @@ -0,0 +1 @@ +{"id":"rawr.png","size":23777,"created":"2020-06-04T16:04:10.085680927Z","file_type":"image/png","hash":"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"} \ No newline at end of file diff --git a/file-service/fixtures/.thumbnails/rawr.png b/file-service/fixtures/.thumbnails/rawr.png new file mode 100644 index 0000000..94ecf15 Binary files /dev/null and b/file-service/fixtures/.thumbnails/rawr.png differ diff --git a/file-service/src/html.rs b/file-service/src/html.rs new file mode 100644 index 0000000..d83b34a --- /dev/null +++ b/file-service/src/html.rs @@ -0,0 +1,135 @@ +use build_html::{self, Html, HtmlContainer}; + +#[derive(Clone, Debug)] +pub struct Form { + method: String, + encoding: Option, + elements: String, +} + +impl Form { + pub fn new() -> Self { + Self { + method: "get".to_owned(), + encoding: None, + elements: "".to_owned(), + } + } + + pub fn with_method(mut self, method: &str) -> Self { + self.method = method.to_owned(); + self + } + + pub fn with_encoding(mut self, encoding: &str) -> Self { + self.encoding = Some(encoding.to_owned()); + self + } +} + +impl Html for Form { + fn to_html_string(&self) -> String { + let encoding = match self.encoding { + Some(ref encoding) => format!("encoding={encoding}", encoding = encoding), + None => format!(""), + }; + format!( + "
\n{elements}\n
\n", + method = self.method, + encoding = encoding, + elements = self.elements.to_html_string() + ) + } +} + +impl HtmlContainer for Form { + fn add_html(&mut self, html: H) { + self.elements.push_str(&html.to_html_string()); + } +} + +#[derive(Clone, Debug)] +pub struct Input { + ty: String, + name: String, + id: String, + value: Option, +} + +impl Html for Input { + fn to_html_string(&self) -> String { + format!( + "{value}\n", + ty = self.ty, + name = self.name, + id = self.id, + value = self.value.clone().unwrap_or("".to_owned()), + ) + } +} + +impl Input { + pub fn new(ty: &str, name: &str, id: &str) -> Self { + Self { + ty: ty.to_owned(), + name: name.to_owned(), + id: id.to_owned(), + value: None, + } + } + + pub fn with_value(mut self, val: &str) -> Self { + self.value = Some(val.to_owned()); + self + } +} + +#[derive(Clone, Debug)] +pub struct Label { + target: String, + text: String, +} + +impl Label { + pub fn new(target: &str, text: &str) -> Self { + Self { + target: target.to_owned(), + text: text.to_owned(), + } + } +} + +impl Html for Label { + fn to_html_string(&self) -> String { + format!( + "", + target = self.target, + text = self.text + ) + } +} + +#[derive(Clone, Debug)] +pub struct Button { + name: String, + text: String, +} + +impl Button { + pub fn new(name: &str, text: &str) -> Self { + Self { + name: name.to_owned(), + text: text.to_owned(), + } + } +} + +impl Html for Button { + fn to_html_string(&self) -> String { + format!( + "", + name = self.name, + text = self.text + ) + } +} diff --git a/file-service/src/lib/error.rs b/file-service/src/lib/error.rs deleted file mode 100644 index 53dc55f..0000000 --- a/file-service/src/lib/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::PathBuf; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("not implemented")] - NotImplemented, - - #[error("file not found: `{0}`")] - FileNotFound(PathBuf), - - #[error("file is not an image: `{0}`")] - NotAnImage(PathBuf), - - #[error("path is not a file: `{0}`")] - NotAFile(PathBuf), - - #[error("Image loading error")] - ImageError(#[from] image::ImageError), - - #[error("IO error")] - IOError(#[from] std::io::Error), - - #[error("JSON error")] - JSONError(#[from] serde_json::error::Error), - - #[error("UTF8 Error")] - UTF8Error(#[from] std::str::Utf8Error), -} - -pub type Result = std::result::Result; diff --git a/file-service/src/lib/file.rs b/file-service/src/lib/file.rs index 3a085fd..fad9e08 100644 --- a/file-service/src/lib/file.rs +++ b/file-service/src/lib/file.rs @@ -1,9 +1,38 @@ -use super::error::{Error, Result}; use super::fileinfo::FileInfo; use super::thumbnail::Thumbnail; use std::fs::{copy, read_dir, remove_file}; use std::path::{Path, PathBuf}; +use thiserror::Error; +#[derive(Error, Debug)] +pub enum FileError { + #[error("not implemented")] + NotImplemented, + + #[error("file not found: `{0}`")] + FileNotFound(PathBuf), + + #[error("file is not an image: `{0}`")] + NotAnImage(PathBuf), + + #[error("path is not a file: `{0}`")] + NotAFile(PathBuf), + + #[error("Image loading error")] + ImageError(#[from] image::ImageError), + + #[error("IO error")] + IOError(#[from] std::io::Error), + + #[error("JSON error")] + JSONError(#[from] serde_json::error::Error), + + #[error("UTF8 Error")] + UTF8Error(#[from] std::str::Utf8Error), +} + +/// 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 { info: FileInfo, @@ -17,7 +46,7 @@ impl File { root: &Path, temp_path: &PathBuf, filename: &Option, - ) -> Result { + ) -> Result { let mut dest_path = PathBuf::from(root); dest_path.push(id); match filename { @@ -33,27 +62,27 @@ impl File { copy(temp_path, dest_path.clone())?; let info = FileInfo::from_path(&dest_path)?; let tn = Thumbnail::from_path(&dest_path)?; - Ok(File { + Ok(Self { info, tn, root: PathBuf::from(root), }) } - pub fn open(id: &str, root: &Path) -> Result { + 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(Error::FileNotFound(file_path)); + return Err(FileError::FileNotFound(file_path)); } if !file_path.is_file() { - return Err(Error::NotAFile(file_path)); + return Err(FileError::NotAFile(file_path)); } let info = match FileInfo::open(id, root) { Ok(i) => Ok(i), - Err(Error::FileNotFound(_)) => { + Err(FileError::FileNotFound(_)) => { let info = FileInfo::from_path(&file_path)?; info.save(&root)?; Ok(info) @@ -63,14 +92,14 @@ impl File { let tn = Thumbnail::open(id, root)?; - Ok(File { + Ok(Self { info, tn, root: PathBuf::from(root), }) } - pub fn list(root: &Path) -> Vec> { + pub fn list(root: &Path) -> Vec> { let dir_iter = read_dir(&root).unwrap(); dir_iter .filter(|entry| { @@ -81,7 +110,7 @@ impl File { .map(|entry| { let entry_ = entry.unwrap(); let id = entry_.file_name().into_string().unwrap(); - File::open(&id, root) + Self::open(&id, root) }) .collect() } @@ -94,13 +123,13 @@ impl File { self.tn.clone() } - pub fn stream(&self) -> Result { + pub fn stream(&self) -> Result { let mut path = self.root.clone(); path.push(self.info.id.clone()); - std::fs::File::open(path).map_err(Error::from) + std::fs::File::open(path).map_err(FileError::from) } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let mut path = self.root.clone(); path.push(self.info.id.clone()); remove_file(path)?; diff --git a/file-service/src/lib/fileinfo.rs b/file-service/src/lib/fileinfo.rs index ee8350e..b3e6d49 100644 --- a/file-service/src/lib/fileinfo.rs +++ b/file-service/src/lib/fileinfo.rs @@ -7,8 +7,7 @@ use std::fs::remove_file; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use super::error::{Error, Result}; -use super::utils::append_extension; +use crate::{append_extension, FileError}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { @@ -23,45 +22,45 @@ pub struct FileInfo { } impl FileInfo { - pub fn save(&self, root: &Path) -> Result<()> { + pub fn save(&self, root: &Path) -> Result<(), FileError> { let ser = serde_json::to_string(self).unwrap(); std::fs::File::create(FileInfo::metadata_path(&self.id, root)) .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) - .map_err(Error::from) + .map_err(FileError::from) } - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { let mut buf = Vec::new(); let md_path = FileInfo::metadata_path(id, root); std::fs::File::open(md_path.clone()) .and_then(|mut stream| stream.read_to_end(&mut buf)) .map_err(move |err| match err.kind() { - std::io::ErrorKind::NotFound => Error::FileNotFound(md_path), - _ => Error::IOError(err), + std::io::ErrorKind::NotFound => FileError::FileNotFound(md_path), + _ => FileError::IOError(err), })?; let str_repr = std::str::from_utf8(&buf)?; - serde_json::from_str(&str_repr).map_err(Error::from) + serde_json::from_str(&str_repr).map_err(FileError::from) } - pub fn from_path(path: &Path) -> Result { + pub fn from_path(path: &Path) -> Result { match (path.is_file(), path.is_dir()) { - (false, false) => Err(Error::FileNotFound(PathBuf::from(path))), - (false, true) => Err(Error::NotAFile(PathBuf::from(path))), + (false, false) => Err(FileError::FileNotFound(PathBuf::from(path))), + (false, true) => Err(FileError::NotAFile(PathBuf::from(path))), (true, _) => Ok(()), }?; - let metadata = path.metadata().map_err(Error::IOError)?; + let metadata = path.metadata().map_err(FileError::IOError)?; let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(Error::NotAFile(PathBuf::from(path)))?; + .ok_or(FileError::NotAFile(PathBuf::from(path)))?; let created = metadata .created() .map(|m| DateTime::from(m)) - .map_err(|err| Error::IOError(err))?; + .map_err(|err| FileError::IOError(err))?; let file_type = String::from( mime_guess::from_path(path) .first_or_octet_stream() @@ -71,18 +70,18 @@ impl FileInfo { Ok(FileInfo { id, size: metadata.len(), - created: created, + created, file_type, hash: hash.as_string(), root: PathBuf::from(path.parent().unwrap()), }) } - fn hash_file(path: &Path) -> Result { + fn hash_file(path: &Path) -> Result { let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(Error::from)?; + let mut file = std::fs::File::open(path).map_err(FileError::from)?; - file.read_to_end(&mut buf).map_err(Error::from)?; + file.read_to_end(&mut buf).map_err(FileError::from)?; let mut vec = Vec::new(); vec.extend_from_slice(Sha256::digest(&buf).as_slice()); Ok(HexString::from_bytes(&vec)) @@ -95,9 +94,9 @@ impl FileInfo { append_extension(&path, "json") } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let path = FileInfo::metadata_path(&self.id, &self.root); - remove_file(path).map_err(Error::from) + remove_file(path).map_err(FileError::from) } } diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs index 40e80c2..3c9d0af 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/lib/mod.rs @@ -1,14 +1,12 @@ use std::path::{Path, PathBuf}; use uuid::Uuid; -mod error; mod file; mod fileinfo; mod thumbnail; -mod utils; +pub mod utils; -pub use error::{Error, Result}; -pub use file::File; +pub use file::{File, FileError}; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; @@ -23,32 +21,36 @@ impl App { } } - pub fn list_files(&self) -> Vec> { + pub fn list_files(&self) -> Vec> { File::list(&self.files_root) } - pub fn add_file(&mut self, temp_path: &PathBuf, filename: &Option) -> Result { + pub fn add_file( + &mut self, + temp_path: &PathBuf, + filename: &Option, + ) -> Result { let id = Uuid::new_v4().hyphenated().to_string(); File::new(&id, &self.files_root, temp_path, filename) } - pub fn delete_file(&mut self, id: String) -> Result<()> { + pub fn delete_file(&mut self, id: String) -> Result<(), FileError> { let f = File::open(&id, &self.files_root)?; f.delete() } - pub fn get_metadata(&self, id: String) -> Result { + pub fn get_metadata(&self, id: String) -> Result { FileInfo::open(&id, &self.files_root) } - pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File)> { + pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File), FileError> { let f = File::open(&id, &self.files_root)?; let info = f.info(); let stream = f.stream()?; Ok((info, stream)) } - pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File)> { + pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> { let f = File::open(id, &self.files_root)?; let stream = f.thumbnail().stream()?; Ok((f.info(), stream)) diff --git a/file-service/src/lib/thumbnail.rs b/file-service/src/lib/thumbnail.rs index 738b4aa..1e458ad 100644 --- a/file-service/src/lib/thumbnail.rs +++ b/file-service/src/lib/thumbnail.rs @@ -2,7 +2,7 @@ use image::imageops::FilterType; use std::fs::remove_file; use std::path::{Path, PathBuf}; -use super::error::{Error, Result}; +use crate::FileError; #[derive(Clone, Debug, PartialEq)] pub struct Thumbnail { @@ -11,7 +11,7 @@ pub struct Thumbnail { } impl Thumbnail { - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { let mut source_path = PathBuf::from(root); source_path.push(id); @@ -30,15 +30,15 @@ impl Thumbnail { Ok(self_) } - pub fn from_path(path: &Path) -> Result { + pub fn from_path(path: &Path) -> Result { let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(Error::NotAnImage(PathBuf::from(path)))?; + .ok_or(FileError::NotAnImage(PathBuf::from(path)))?; let root = path .parent() - .ok_or(Error::FileNotFound(PathBuf::from(path)))?; + .ok_or(FileError::FileNotFound(PathBuf::from(path)))?; Thumbnail::open(&id, root) } @@ -50,20 +50,20 @@ impl Thumbnail { path } - pub fn stream(&self) -> Result { + pub fn stream(&self) -> Result { let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root); std::fs::File::open(thumbnail_path.clone()).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { - Error::FileNotFound(thumbnail_path) + FileError::FileNotFound(thumbnail_path) } else { - Error::from(err) + FileError::from(err) } }) } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let path = Thumbnail::thumbnail_path(&self.id, &self.root); - remove_file(path).map_err(Error::from) + remove_file(path).map_err(FileError::from) } } diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 67f1401..addd532 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -1,28 +1,31 @@ +/* use iron::headers; use iron::middleware::Handler; use iron::modifiers::{Header, Redirect}; use iron::prelude::*; use iron::response::BodyReader; use iron::status; -use mustache::{compile_path, Template}; -use orizentic::{Permissions, ResourceName, Secret}; -use params::{Params, Value}; -use router::Router; -use serde::Serialize; -use std::collections::HashMap; -use std::fs::File; -use std::io::Read; -use std::path::Path; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; +*/ +use http::status::StatusCode; +// use mustache::{compile_path, Template}; +// use orizentic::{Permissions, ResourceName, Secret}; +use build_html::Html; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::Path, + sync::{Arc, RwLock}, +}; +use warp::Filter; mod cookies; +mod html; mod lib; mod middleware; +mod pages; -use lib::{App, FileInfo}; -use middleware::{Authentication, RestForm}; +use lib::{utils::append_extension, App, File, FileError, FileInfo}; +/* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { let Permissions(perms) = permissions; ResourceName(String::from( @@ -280,18 +283,17 @@ fn script(_: &mut Request) -> IronResult { js, ))) } +*/ -fn main() { +#[tokio::main] +pub async fn main() { + /* let auth_db_path = std::env::var("ORIZENTIC_DB").unwrap(); let secret = Secret(Vec::from( std::env::var("ORIZENTIC_SECRET").unwrap().as_bytes(), )); let auth_middleware = Authentication::new(secret, auth_db_path); - let app = Arc::new(RwLock::new(App::new(Path::new( - &std::env::var("FILE_SHARE_DIR").unwrap(), - )))); - let mut router = Router::new(); router.get( "/", @@ -338,4 +340,39 @@ fn main() { chain.link_before(RestForm {}); Iron::new(chain).http("0.0.0.0:3000").unwrap(); + */ + + /* + let root = warp::path!().and(warp::get()).map({ + || { + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::NOT_MODIFIED) + .body(()) + } + }); + let server = warp::serve(root); + server + .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) + .await; + */ + + let app = Arc::new(RwLock::new(App::new(Path::new( + &std::env::var("FILE_SHARE_DIR").unwrap(), + )))); + + let root = warp::path!().map({ + let app = app.clone(); + move || { + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::OK) + .body(pages::index(app.read().unwrap().list_files()).to_html_string()) + } + }); + + let server = warp::serve(root); + server + .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) + .await; } diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs new file mode 100644 index 0000000..e1385df --- /dev/null +++ b/file-service/src/pages.rs @@ -0,0 +1,40 @@ +use crate::{html::*, File, FileError}; +use build_html::{self, Container, ContainerType, HtmlContainer}; + +pub fn index(files: Vec>) -> build_html::HtmlPage { + let mut page = build_html::HtmlPage::new() + .with_title("Admin list of files") + .with_header(1, "Admin list of files") + .with_html( + Form::new() + .with_method("post") + .with_encoding("multipart/form-data") + .with_container( + Container::new(ContainerType::Div) + .with_html(Input::new("file", "file", "file-selector-input")) + .with_html(Label::new("for-selector-input", "Select a file")), + ) + .with_html(Button::new("upload", "Upload file")), + ); + + for file in files { + let mut container = + Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); + match file { + Ok(file) => { + let tn = Container::new(ContainerType::Div) + .with_attributes(vec![("class", "thumbnail")]) + .with_link( + format!("/file/{}", file.info().id), + "

paragraph within the link

".to_owned(), + ); + container.add_html(tn); + } + Err(err) => { + container.add_paragraph(format!("{:?}", err)); + } + } + page.add_container(container); + } + page +}