Import a questionably refactored version of file-service

This commit is contained in:
Savanni D'Gerinel 2023-09-19 18:55:53 -04:00
parent 356395a503
commit 81aa7410de
22 changed files with 2618 additions and 0 deletions

1404
file-service/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
file-service/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "file-service"
version = "0.1.0"
authors = ["savanni@luminescent-dreams.com"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
hex-string = "0.1.0"
iron = "0.6.1"
logger = "*"
mime = "0.3.16"
mime_guess = "2.0.3"
mustache = "0.9.0"
orizentic = "1.0.0"
params = "*"
router = "*"
serde_json = "*"
serde = { version = "1.0", features = ["derive"] }
sha2 = "0.8.2"
uuid = { version = "0.4", features = ["serde", "v4"] }
thiserror = "1.0.20"
image = "0.23.5"

1
file-service/authdb.json Normal file
View File

@ -0,0 +1 @@
[{"jti":"ac3a46c6-3fa1-4d0a-af12-e7d3fefdc878","aud":"savanni","exp":1621351436,"iss":"savanni","iat":1589729036,"sub":"https://savanni.luminescent-dreams.com/file-service/","perms":["admin"]}]

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,61 @@
use iron::headers;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Cookie {
pub name: String,
pub value: String,
}
impl From<&str> for Cookie {
fn from(s: &str) -> Cookie {
let parts: Vec<&str> = s.split("=").collect();
Cookie {
name: String::from(parts[0]),
value: String::from(parts[1]),
}
}
}
impl From<&String> for Cookie {
fn from(s: &String) -> Cookie {
Cookie::from(s.as_str())
}
}
impl From<String> for Cookie {
fn from(s: String) -> Cookie {
Cookie::from(s.as_str())
}
}
#[derive(Debug)]
pub struct CookieJar(HashMap<String, Cookie>);
impl CookieJar {
pub fn new() -> CookieJar {
CookieJar(HashMap::new())
}
pub fn add_cookie(&mut self, name: String, value: Cookie) {
self.0.insert(name, value);
}
pub fn lookup(&self, name: &str) -> Option<&Cookie> {
self.0.get(name)
}
}
// Some(Cookie(["auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhYzNhNDZjNi0zZmExLTRkMGEtYWYxMi1lN2QzZmVmZGM4NzgiLCJhdWQiOiJzYXZhbm5pIiwiZXhwIjoxNjIxMzUxNDM2LCJpc3MiOiJzYXZhbm5pIiwiaWF0IjoxNTg5NzI5MDM2LCJzdWIiOiJodHRwczovL3NhdmFubmkubHVtaW5lc2NlbnQtZHJlYW1zLmNvbS9maWxlLXNlcnZpY2UvIiwicGVybXMiOlsiYWRtaW4iXX0.8zjAbZ7Ut0d6EcDeyik39GKhXvH4qkMDdaiQVNKWiuM"]))
impl From<&headers::Cookie> for CookieJar {
fn from(c: &headers::Cookie) -> CookieJar {
let jar = CookieJar::new();
let headers::Cookie(cs) = c;
cs.iter().fold(jar, |mut jar, c_| {
let cookie = Cookie::from(c_);
jar.add_cookie(cookie.name.clone(), cookie);
jar
})
}
}

View File

@ -0,0 +1,31 @@
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<A> = std::result::Result<A, Error>;

View File

@ -0,0 +1,151 @@
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};
#[derive(Debug)]
pub struct File {
info: FileInfo,
tn: Thumbnail,
root: PathBuf,
}
impl File {
pub fn new(
id: &str,
root: &Path,
temp_path: &PathBuf,
filename: &Option<PathBuf>,
) -> Result<File> {
let mut dest_path = PathBuf::from(root);
dest_path.push(id);
match filename {
Some(fname) => match fname.extension() {
Some(ext) => {
dest_path.set_extension(ext);
()
}
None => (),
},
None => (),
};
copy(temp_path, dest_path.clone())?;
let info = FileInfo::from_path(&dest_path)?;
let tn = Thumbnail::from_path(&dest_path)?;
Ok(File {
info,
tn,
root: PathBuf::from(root),
})
}
pub fn open(id: &str, root: &Path) -> Result<File> {
let mut file_path = PathBuf::from(root);
file_path.push(id.clone());
if !file_path.exists() {
return Err(Error::FileNotFound(file_path));
}
if !file_path.is_file() {
return Err(Error::NotAFile(file_path));
}
let info = match FileInfo::open(id, root) {
Ok(i) => Ok(i),
Err(Error::FileNotFound(_)) => {
let info = FileInfo::from_path(&file_path)?;
info.save(&root)?;
Ok(info)
}
Err(err) => Err(err),
}?;
let tn = Thumbnail::open(id, root)?;
Ok(File {
info,
tn,
root: PathBuf::from(root),
})
}
pub fn list(root: &Path) -> Vec<Result<File>> {
let dir_iter = read_dir(&root).unwrap();
dir_iter
.filter(|entry| {
let entry_ = entry.as_ref().unwrap();
let filename = entry_.file_name();
!(filename.to_string_lossy().starts_with("."))
})
.map(|entry| {
let entry_ = entry.unwrap();
let id = entry_.file_name().into_string().unwrap();
File::open(&id, root)
})
.collect()
}
pub fn info(&self) -> FileInfo {
self.info.clone()
}
pub fn thumbnail(&self) -> Thumbnail {
self.tn.clone()
}
pub fn stream(&self) -> Result<std::fs::File> {
let mut path = self.root.clone();
path.push(self.info.id.clone());
std::fs::File::open(path).map_err(Error::from)
}
pub fn delete(&self) -> Result<()> {
let mut path = self.root.clone();
path.push(self.info.id.clone());
remove_file(path)?;
self.tn.delete()?;
self.info.delete()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::lib::utils::FileCleanup;
use std::path::{Path, PathBuf};
#[test]
fn it_opens_a_file() {
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json"));
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
}
#[test]
fn it_can_return_a_thumbnail() {
let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
assert_eq!(
f.thumbnail(),
Thumbnail {
id: String::from("rawr.png"),
root: PathBuf::from("fixtures/"),
},
);
}
#[test]
fn it_can_return_a_file_stream() {
let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
f.stream().expect("to succeed");
}
#[test]
fn it_raises_an_error_when_file_not_found() {
match File::open("garbage", Path::new("fixtures/")) {
Err(Error::FileNotFound(_)) => assert!(true),
_ => assert!(false),
}
}
}

View File

@ -0,0 +1,160 @@
use chrono::prelude::*;
use hex_string::HexString;
use serde::{Deserialize, Serialize};
use serde_json;
use sha2::{Digest, Sha256};
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;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileInfo {
pub id: String,
pub size: u64,
pub created: DateTime<Utc>,
pub file_type: String,
pub hash: String,
#[serde(skip)]
root: PathBuf,
}
impl FileInfo {
pub fn save(&self, root: &Path) -> Result<()> {
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)
}
pub fn open(id: &str, root: &Path) -> Result<FileInfo> {
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),
})?;
let str_repr = std::str::from_utf8(&buf)?;
serde_json::from_str(&str_repr).map_err(Error::from)
}
pub fn from_path(path: &Path) -> Result<FileInfo> {
match (path.is_file(), path.is_dir()) {
(false, false) => Err(Error::FileNotFound(PathBuf::from(path))),
(false, true) => Err(Error::NotAFile(PathBuf::from(path))),
(true, _) => Ok(()),
}?;
let metadata = path.metadata().map_err(Error::IOError)?;
let id = path
.file_name()
.map(|s| String::from(s.to_string_lossy()))
.ok_or(Error::NotAFile(PathBuf::from(path)))?;
let created = metadata
.created()
.map(|m| DateTime::from(m))
.map_err(|err| Error::IOError(err))?;
let file_type = String::from(
mime_guess::from_path(path)
.first_or_octet_stream()
.essence_str(),
);
let hash = FileInfo::hash_file(path)?;
Ok(FileInfo {
id,
size: metadata.len(),
created: created,
file_type,
hash: hash.as_string(),
root: PathBuf::from(path.parent().unwrap()),
})
}
fn hash_file(path: &Path) -> Result<HexString> {
let mut buf = Vec::new();
let mut file = std::fs::File::open(path).map_err(Error::from)?;
file.read_to_end(&mut buf).map_err(Error::from)?;
let mut vec = Vec::new();
vec.extend_from_slice(Sha256::digest(&buf).as_slice());
Ok(HexString::from_bytes(&vec))
}
fn metadata_path(id: &str, root: &Path) -> PathBuf {
let mut path = PathBuf::from(root);
path.push(".metadata");
path.push(id.clone());
append_extension(&path, "json")
}
pub fn delete(&self) -> Result<()> {
let path = FileInfo::metadata_path(&self.id, &self.root);
remove_file(path).map_err(Error::from)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::lib::utils::FileCleanup;
#[test]
fn it_generates_information_from_file_path() {
let path = Path::new("fixtures/rawr.png");
match FileInfo::from_path(&path) {
Ok(FileInfo {
id,
size,
file_type,
hash,
..
}) => {
assert_eq!(id, "rawr.png");
assert_eq!(size, 23777);
assert_eq!(file_type, "image/png");
assert_eq!(
hash,
"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"
);
}
Err(err) => {
println!("error loading file path: {}", err);
assert!(false);
}
}
}
#[test]
fn it_saves_and_loads_metadata() {
let path = Path::new("fixtures/rawr.png");
let _ = FileCleanup(append_extension(path, "json"));
let info = FileInfo::from_path(&path).unwrap();
info.save(Path::new("fixtures")).unwrap();
assert!(Path::new("fixtures/.metadata/rawr.png.json").is_file());
let info_ = FileInfo::open("rawr.png", Path::new("fixtures")).unwrap();
assert_eq!(info_.id, "rawr.png");
assert_eq!(info_.size, 23777);
assert_eq!(info_.created, info.created);
assert_eq!(info_.file_type, "image/png");
assert_eq!(info_.hash, info.hash);
}
#[test]
fn it_extends_a_file_extension() {
assert_eq!(
append_extension(Path::new("fixtures/rawr.png"), "json"),
Path::new("fixtures/rawr.png.json")
);
}
}

View File

@ -0,0 +1,56 @@
use std::path::{Path, PathBuf};
use uuid::Uuid;
mod error;
mod file;
mod fileinfo;
mod thumbnail;
mod utils;
pub use error::{Error, Result};
pub use file::File;
pub use fileinfo::FileInfo;
pub use thumbnail::Thumbnail;
pub struct App {
files_root: PathBuf,
}
impl App {
pub fn new(files_root: &Path) -> App {
App {
files_root: PathBuf::from(files_root),
}
}
pub fn list_files(&self) -> Vec<Result<File>> {
File::list(&self.files_root)
}
pub fn add_file(&mut self, temp_path: &PathBuf, filename: &Option<PathBuf>) -> Result<File> {
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<()> {
let f = File::open(&id, &self.files_root)?;
f.delete()
}
pub fn get_metadata(&self, id: String) -> Result<FileInfo> {
FileInfo::open(&id, &self.files_root)
}
pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File)> {
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)> {
let f = File::open(id, &self.files_root)?;
let stream = f.thumbnail().stream()?;
Ok((f.info(), stream))
}
}

View File

@ -0,0 +1,82 @@
use image::imageops::FilterType;
use std::fs::remove_file;
use std::path::{Path, PathBuf};
use super::error::{Error, Result};
#[derive(Clone, Debug, PartialEq)]
pub struct Thumbnail {
pub id: String,
pub root: PathBuf,
}
impl Thumbnail {
pub fn open(id: &str, root: &Path) -> Result<Thumbnail> {
let mut source_path = PathBuf::from(root);
source_path.push(id);
let self_ = Thumbnail {
id: String::from(id),
root: PathBuf::from(root),
};
let thumbnail_path = Thumbnail::thumbnail_path(id, root);
if !thumbnail_path.exists() {
let img = image::open(source_path)?;
let tn = img.resize(640, 640, FilterType::Nearest);
tn.save(thumbnail_path)?;
}
Ok(self_)
}
pub fn from_path(path: &Path) -> Result<Thumbnail> {
let id = path
.file_name()
.map(|s| String::from(s.to_string_lossy()))
.ok_or(Error::NotAnImage(PathBuf::from(path)))?;
let root = path
.parent()
.ok_or(Error::FileNotFound(PathBuf::from(path)))?;
Thumbnail::open(&id, root)
}
fn thumbnail_path(id: &str, root: &Path) -> PathBuf {
let mut path = PathBuf::from(root);
path.push(".thumbnails");
path.push(id.clone());
path
}
pub fn stream(&self) -> Result<std::fs::File> {
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)
} else {
Error::from(err)
}
})
}
pub fn delete(&self) -> Result<()> {
let path = Thumbnail::thumbnail_path(&self.id, &self.root);
remove_file(path).map_err(Error::from)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::lib::utils::FileCleanup;
#[test]
fn it_creates_a_thumbnail_if_one_does_not_exist() {
let _ = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
let _ =
Thumbnail::open("rawr.png", Path::new("fixtures")).expect("thumbnail open must work");
assert!(Path::new("fixtures/.thumbnails/rawr.png").is_file());
}
}

View File

@ -0,0 +1,17 @@
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_)
}

341
file-service/src/main.rs Normal file
View File

@ -0,0 +1,341 @@
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};
mod cookies;
mod lib;
mod middleware;
use lib::{App, FileInfo};
use middleware::{Authentication, RestForm};
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
let Permissions(perms) = permissions;
ResourceName(String::from(
"https://savanni.luminescent-dreams.com/file-service/",
)) == *resource
&& perms.contains(&String::from("admin"))
}
pub fn compare_etags(info: FileInfo, etag_list: &headers::IfNoneMatch) -> bool {
let current_etag = headers::EntityTag::new(false, info.hash);
match etag_list {
headers::IfNoneMatch::Any => false,
headers::IfNoneMatch::Items(lst) => lst.iter().any(|etag| etag.weak_eq(&current_etag)),
}
}
mod files {
use super::*;
pub struct IndexHandler {
pub app: Arc<RwLock<App>>,
pub template: Template,
}
#[derive(Serialize)]
pub enum TemplateFile {
#[serde(rename = "error")]
Error { error: String },
#[serde(rename = "file")]
File {
id: String,
size: u64,
date: String,
type_: String,
},
}
#[derive(Serialize)]
pub struct IndexTemplateParams {
files: Vec<TemplateFile>,
}
impl Handler for IndexHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let app = self.app.read().unwrap();
let m_token = req.extensions.get::<Authentication>();
match m_token {
Some(token) => {
if token.check_authorizations(is_admin) {
let files: Vec<TemplateFile> = app
.list_files()
.into_iter()
.map(|entry| match entry {
Ok(file) => TemplateFile::File {
id: file.info().id,
size: file.info().size,
date: format!(
"{}",
file.info().created.format("%Y-%m-%d %H:%M:%S")
),
type_: file.info().file_type,
},
Err(err) => TemplateFile::Error {
error: format!("{}", err),
},
})
.collect();
Ok(Response::with((
status::Ok,
Header(headers::ContentType::html()),
Header(headers::SetCookie(vec![format!("auth={}", token.text)])),
self.template
.render_to_string(&IndexTemplateParams { files })
.expect("the template to render"),
)))
} else {
Ok(Response::with(status::Forbidden))
}
}
None => Ok(Response::with(status::Forbidden)),
}
}
}
pub struct GetHandler {
pub app: Arc<RwLock<App>>,
}
impl Handler for GetHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let app = self.app.read().unwrap();
let capture = req.extensions.get::<Router>().unwrap().clone();
let old_etags = req.headers.get::<headers::IfNoneMatch>();
match capture.find("id") {
Some(id) => {
let info = app.get_metadata(String::from(id));
match (info, old_etags) {
(Ok(info_), Some(if_none_match)) => {
if compare_etags(info_, if_none_match) {
return Ok(Response::with(status::NotModified));
}
}
_ => (),
}
match app.get_file(String::from(id)) {
Ok((info, stream)) => Ok(Response::with((
status::Ok,
Header(headers::ContentType(
info.file_type.parse::<iron::mime::Mime>().unwrap(),
)),
Header(headers::ETag(headers::EntityTag::new(false, info.hash))),
BodyReader(stream),
))),
Err(_err) => Ok(Response::with(status::NotFound)),
}
}
_ => Ok(Response::with(status::BadRequest)),
}
}
}
pub struct GetThumbnailHandler {
pub app: Arc<RwLock<App>>,
}
impl Handler for GetThumbnailHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let app = self.app.read().unwrap();
let capture = req.extensions.get::<Router>().unwrap().clone();
let old_etags = req.headers.get::<headers::IfNoneMatch>();
match capture.find("id") {
Some(id) => {
let info = app.get_metadata(String::from(id));
match (info, old_etags) {
(Ok(info_), Some(if_none_match)) => {
if compare_etags(info_, if_none_match) {
return Ok(Response::with(status::NotModified));
}
}
_ => (),
}
match app.get_thumbnail(id) {
Ok((info, stream)) => Ok(Response::with((
status::Ok,
Header(headers::ContentType(
info.file_type.parse::<iron::mime::Mime>().unwrap(),
)),
Header(headers::ETag(headers::EntityTag::new(false, info.hash))),
BodyReader(stream),
))),
Err(_err) => Ok(Response::with(status::NotFound)),
}
}
_ => Ok(Response::with(status::BadRequest)),
}
}
}
pub struct PostHandler {
pub app: Arc<RwLock<App>>,
}
impl Handler for PostHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let mut app = self.app.write().unwrap();
let m_token = req.extensions.get::<Authentication>();
match m_token {
Some(token) => {
if token.check_authorizations(is_admin) {
let params = req.get_ref::<Params>().unwrap();
if let Value::File(f_info) = params.get("file").unwrap() {
match app.add_file(
&f_info.path,
&f_info.filename.clone().map(|fname| PathBuf::from(fname)),
) {
Ok(_) => Ok(Response::with((
status::MovedPermanently,
Redirect(router::url_for(req, "index", HashMap::new())),
))),
Err(_) => Ok(Response::with(status::InternalServerError)),
}
} else {
Ok(Response::with(status::BadRequest))
}
} else {
Ok(Response::with(status::Forbidden))
}
}
None => Ok(Response::with(status::Forbidden)),
}
}
}
pub struct DeleteHandler {
pub app: Arc<RwLock<App>>,
}
impl Handler for DeleteHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let mut app = self.app.write().unwrap();
let capture = req.extensions.get::<Router>().unwrap().clone();
let m_token = req.extensions.get::<Authentication>();
match m_token {
Some(token) => {
if token.check_authorizations(is_admin) {
match capture.find("id") {
Some(id) => match app.delete_file(String::from(id)) {
Ok(()) => Ok(Response::with((
status::MovedPermanently,
Redirect(router::url_for(req, "index", HashMap::new())),
))),
Err(_) => Ok(Response::with(status::InternalServerError)),
},
None => Ok(Response::with(status::BadRequest)),
}
} else {
Ok(Response::with(status::Forbidden))
}
}
None => Ok(Response::with(status::Forbidden)),
}
}
}
}
fn css(_: &mut Request) -> IronResult<Response> {
let mut css: String = String::from("");
File::open("templates/style.css")
.unwrap()
.read_to_string(&mut css)
.unwrap();
Ok(Response::with((
status::Ok,
Header(headers::ContentType(iron::mime::Mime(
iron::mime::TopLevel::Text,
iron::mime::SubLevel::Css,
vec![],
))),
css,
)))
}
fn script(_: &mut Request) -> IronResult<Response> {
let mut js: String = String::from("");
File::open("templates/script.js")
.unwrap()
.read_to_string(&mut js)
.unwrap();
Ok(Response::with((
status::Ok,
Header(headers::ContentType(iron::mime::Mime(
iron::mime::TopLevel::Text,
iron::mime::SubLevel::Javascript,
vec![],
))),
js,
)))
}
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(
"/",
files::IndexHandler {
app: app.clone(),
template: compile_path("templates/index.html").expect("the template to compile"),
},
"index",
);
router.get(
"/:id",
files::GetFileHandler {
app: app.clone(),
template: compile_path("templates/file.html").expect("the template to compile"),
},
"get-file-page",
);
router.get(
"/:id/raw",
files::GetHandler { app: app.clone() },
"get-file",
);
router.get(
"/:id/tn",
files::GetThumbnailHandler { app: app.clone() },
"get-thumbnail",
);
router.post("/", files::PostHandler { app: app.clone() }, "upload-file");
router.delete(
"/:id",
files::DeleteHandler { app: app.clone() },
"delete-file",
);
router.get("/css", css, "styles");
router.get("/script", script, "script");
let mut chain = Chain::new(router);
chain.link_before(auth_middleware);
chain.link_before(RestForm {});
Iron::new(chain).http("0.0.0.0:3000").unwrap();
}

View File

@ -0,0 +1,51 @@
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<orizentic::VerifiedToken, orizentic::Error> {
self.auth.decode_and_validate_text(&token_str)
}
}
impl BeforeMiddleware for Authentication {
fn before(&self, req: &mut Request) -> IronResult<()> {
let params = req.get_ref::<Params>().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::<headers::Cookie>()
.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::<Authentication>(t));
Ok(())
}
}

View File

@ -0,0 +1,16 @@
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<Response> {
Ok(res)
}
}

View File

@ -0,0 +1,6 @@
mod authentication;
mod logging;
mod restform;
pub use authentication::Authentication;
pub use restform::RestForm;

View File

@ -0,0 +1,34 @@
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<Method> {
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::<Params>().unwrap();
params
.get("_method")
.and_then(|m| self.method(m))
.unwrap_or(Method::Post)
};
req.method = method;
}
Ok(())
}
}

View File

@ -0,0 +1,15 @@
<html>
<head>
<title> {{title}} </title>
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
<script src="/script"></script>
</head>
<body>
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
</body>
</html>

View File

@ -0,0 +1,54 @@
<html>
<head>
<title> Admin list of files </title>
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
<script src="/script"></script>
</head>
<body>
<h1> Admin list of files </h1>
<div class="uploadform">
<form action="/" method="post" enctype="multipart/form-data">
<div id="file-selector">
<input type="file" name="file" id="file-selector-input" />
<label for="file-selector-input" onclick="selectFile('file-selector')">Select a file</label>
</div>
<input type="submit" value="Upload file" />
</form>
</div>
<div class="files">
{{#files}}
<div class="file">
{{#error}}
<div>
<p> {{error}} </p>
</div>
{{/error}}
{{#file}}
<div class="thumbnail">
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
</div>
<div>
<ul>
<li> {{date}} </li>
<li> {{type_}} </li>
<li> {{size}} </li>
</ul>
<div>
<form action="/{{id}}" method="post">
<input type="hidden" name="_method" value="delete" />
<input type="submit" value="Delete" />
</form>
</div>
</div>
{{/file}}
</div>
{{/files}}
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
const selectFile = (selectorId) => {
console.log("wide arrow functions work: " + selectorId);
const input = document.querySelector("#" + selectorId + " input[type='file']")
const label = document.querySelector("#" + selectorId + " label")
input.addEventListener("change", (e) => {
if (input.files.length > 0) {
label.innerHTML = input.files[0].name
}
})
}

View File

@ -0,0 +1,103 @@
body {
font-family: 'Ariel', sans-serif;
}
.files {
display: flex;
flex-wrap: wrap;
}
.file {
display: flex;
margin: 1em;
border: 1px solid #449dfc;
border-radius: 5px;
padding: 1em;
}
.thumbnail {
max-width: 320px;
margin: 1em;
}
img {
max-width: 100%;
}
[type="submit"] {
border-radius: 1em;
margin: 1em;
padding: 1em;
}
.uploadform {
display: flex;
margin: 1em;
background-color: #e5f0fc;
border: 1px solid #449dfc;
border-radius: 5px;
padding: 1em;
}
/* https://benmarshall.me/styling-file-inputs/ */
[type="file"] {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute !important;
white-space: nowrap;
width: 1px;
}
[type="file"] + label {
background-color: rgb(0, 86, 112);
border-radius: 1em;
color: #fff;
cursor: pointer;
display: inline-block;
padding: 1em;
margin: 1em;
transition: background-color 0.3s;
}
[type="file"]:focus + label,
[type="file"] + label:hover {
background-color: #67b0ff;
}
[type="file"]:focus + label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
@media screen and (max-width: 980px) { /* This is the screen width of a OnePlus 5t */
body {
font-size: xx-large;
}
[type="submit"] {
font-size: xx-large;
width: 100%;
}
.uploadform {
display: flex;
}
[type="file"] + label {
width: 100%;
}
.thumbnail {
max-width: 100%;
margin: 1em;
}
.file {
display: flex;
flex-direction: column;
width: 100%;
}
}