From 634c404ae9f70ab8a81fe5404a6b7ac833cc0af3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 20 Sep 2023 23:06:34 -0400 Subject: [PATCH] Swap from iron to warp and start rebuilding the app --- Cargo.lock | 236 +++++++++++++++++- file-service/Cargo.toml | 6 +- file-service/fixtures/.metadata/rawr.png.json | 1 + file-service/fixtures/.thumbnails/rawr.png | Bin 0 -> 48869 bytes file-service/src/html.rs | 135 ++++++++++ file-service/src/lib/error.rs | 31 --- file-service/src/lib/file.rs | 55 +++- file-service/src/lib/fileinfo.rs | 39 ++- file-service/src/lib/mod.rs | 22 +- file-service/src/lib/thumbnail.rs | 20 +- file-service/src/main.rs | 73 ++++-- file-service/src/middleware/authentication.rs | 51 ---- file-service/src/middleware/logging.rs | 16 -- file-service/src/middleware/mod.rs | 6 - file-service/src/middleware/restform.rs | 34 --- file-service/src/pages.rs | 40 +++ 16 files changed, 553 insertions(+), 212 deletions(-) create mode 100644 file-service/fixtures/.metadata/rawr.png.json create mode 100644 file-service/fixtures/.thumbnails/rawr.png create mode 100644 file-service/src/html.rs delete mode 100644 file-service/src/lib/error.rs delete mode 100644 file-service/src/middleware/authentication.rs delete mode 100644 file-service/src/middleware/logging.rs delete mode 100644 file-service/src/middleware/mod.rs delete mode 100644 file-service/src/middleware/restform.rs create mode 100644 file-service/src/pages.rs diff --git a/Cargo.lock b/Cargo.lock index f15073f..6a473bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,15 @@ dependencies = [ "generic-array 0.12.4", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -196,6 +205,12 @@ dependencies = [ "safemem 0.2.0", ] +[[package]] +name = "build_html" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3108fe6fe7ac796fb7625bdde8fa2b67b5a7731496251ca57c7b8cadd78a16a1" + [[package]] name = "bumpalo" version = "3.14.0" @@ -404,6 +419,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -452,6 +476,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + [[package]] name = "cyberpunk-splash" version = "0.1.0" @@ -489,6 +523,12 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "deflate" version = "0.8.6" @@ -508,6 +548,16 @@ dependencies = [ "generic-array 0.12.4", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + [[package]] name = "dimensioned" version = "0.7.0" @@ -639,8 +689,10 @@ dependencies = [ name = "file-service" version = "0.1.0" dependencies = [ + "build_html", "chrono", "hex-string", + "http", "image 0.23.14", "iron", "logger", @@ -654,7 +706,9 @@ dependencies = [ "serde_json", "sha2", "thiserror", + "tokio", "uuid 0.4.0", + "warp", ] [[package]] @@ -940,6 +994,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check 0.9.4", +] + [[package]] name = "geo-types" version = "0.1.0" @@ -1353,6 +1417,30 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.4", + "bytes", + "headers-core", + "http", + "httpdate", + "mime 0.3.17", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1972,6 +2060,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log 0.4.20", + "memchr 2.6.4", + "mime 0.3.17", + "spin", + "version_check 0.9.4", +] + [[package]] name = "multipart" version = "0.13.6" @@ -2432,6 +2538,26 @@ dependencies = [ "siphasher 0.3.11", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2928,6 +3054,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + [[package]] name = "rusty-fork" version = "0.3.0" @@ -2967,6 +3102,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -3091,14 +3232,25 @@ dependencies = [ "typeshare", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.7.3", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -3391,6 +3543,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log 0.4.20", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -3463,6 +3638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log 0.4.20", "pin-project-lite", "tracing-core", ] @@ -3488,6 +3664,25 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log 0.4.20", + "rand 0.8.5", + "sha1", + "thiserror", + "url 2.4.1", + "utf-8", +] + [[package]] name = "twoway" version = "0.1.8" @@ -3667,6 +3862,12 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "0.4.0" @@ -3744,6 +3945,37 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper 0.14.27", + "log 0.4.20", + "mime 0.3.17", + "mime_guess 2.0.4", + "multer", + "percent-encoding 2.3.0", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde 1.0.188", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" 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 0000000000000000000000000000000000000000..94ecf15f050cb1d1226a9de11b72a46ef9910c4b GIT binary patch literal 48869 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

eRTJ3zZ#y(l~ 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 Q4$Og-Y-Sm>SXQTt(g%~sKeFX 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, + 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/middleware/authentication.rs b/file-service/src/middleware/authentication.rs deleted file mode 100644 index 658bee8..0000000 --- a/file-service/src/middleware/authentication.rs +++ /dev/null @@ -1,51 +0,0 @@ -use iron::headers; -use iron::middleware::BeforeMiddleware; -use iron::prelude::*; -use iron::typemap::Key; -use orizentic::{filedb, OrizenticCtx, Secret}; -use params::{FromValue, Params}; - -use crate::cookies::{Cookie, CookieJar}; - -pub struct Authentication { - pub auth: OrizenticCtx, -} - -impl Key for Authentication { - type Value = orizentic::VerifiedToken; -} - -impl Authentication { - pub fn new(secret: Secret, auth_db_path: String) -> Authentication { - let claims = filedb::load_claims_from_file(&auth_db_path).expect("claims did not load"); - let orizentic = OrizenticCtx::new(secret, claims); - Authentication { auth: orizentic } - } - - fn authenticate_user( - &self, - token_str: String, - ) -> Result { - self.auth.decode_and_validate_text(token_str) - } -} - -impl BeforeMiddleware for Authentication { - fn before(&self, req: &mut Request) -> IronResult<()> { - let params = req.get_ref::().unwrap(); - let token = match params.get("auth").and_then(|v| String::from_value(v)) { - Some(token_str) => self.authenticate_user(token_str).ok(), - None => { - let m_jar = req - .headers - .get::() - .map(|cookies| CookieJar::from(cookies)); - m_jar - .and_then(|jar| jar.lookup("auth").cloned()) - .and_then(|Cookie { value, .. }| self.authenticate_user(value.clone()).ok()) - } - }; - token.map(|t| req.extensions.insert::(t)); - Ok(()) - } -} diff --git a/file-service/src/middleware/logging.rs b/file-service/src/middleware/logging.rs deleted file mode 100644 index 2aceb1d..0000000 --- a/file-service/src/middleware/logging.rs +++ /dev/null @@ -1,16 +0,0 @@ -use iron::middleware::{AfterMiddleware, BeforeMiddleware}; -use iron::prelude::*; - -pub struct Logging {} - -impl BeforeMiddleware for Logging { - fn before(&self, _: &mut Request) -> IronResult<()> { - Ok(()) - } -} - -impl AfterMiddleware for Logging { - fn after(&self, _: &mut Request, res: Response) -> IronResult { - Ok(res) - } -} diff --git a/file-service/src/middleware/mod.rs b/file-service/src/middleware/mod.rs deleted file mode 100644 index 1bab46b..0000000 --- a/file-service/src/middleware/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod authentication; -mod logging; -mod restform; - -pub use authentication::Authentication; -pub use restform::RestForm; diff --git a/file-service/src/middleware/restform.rs b/file-service/src/middleware/restform.rs deleted file mode 100644 index 5cfadd9..0000000 --- a/file-service/src/middleware/restform.rs +++ /dev/null @@ -1,34 +0,0 @@ -use iron::method::Method; -use iron::middleware::BeforeMiddleware; -use iron::prelude::*; -use params::{Params, Value}; - -pub struct RestForm {} - -impl RestForm { - fn method(&self, v: &Value) -> Option { - match v { - Value::String(method_str) => match method_str.as_str() { - "delete" => Some(Method::Delete), - _ => None, - }, - _ => None, - } - } -} - -impl BeforeMiddleware for RestForm { - fn before(&self, req: &mut Request) -> IronResult<()> { - if req.method == Method::Post { - let method = { - let params = req.get_ref::().unwrap(); - params - .get("_method") - .and_then(|m| self.method(m)) - .unwrap_or(Method::Post) - }; - req.method = method; - } - Ok(()) - } -} 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 +}