Import and update the file service application and orizentic #72
File diff suppressed because it is too large
Load Diff
|
@ -8,28 +8,27 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
build_html = { version = "2" }
|
build_html = { version = "2" }
|
||||||
|
bytes = { version = "1" }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
futures-util = { version = "0.3" }
|
||||||
hex-string = "0.1.0"
|
hex-string = "0.1.0"
|
||||||
http = { version = "0.2" }
|
http = { version = "0.2" }
|
||||||
image = "0.23.5"
|
image = "0.23.5"
|
||||||
iron = "0.6.1"
|
|
||||||
logger = "*"
|
logger = "*"
|
||||||
|
log = { version = "0.4" }
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
mime_guess = "2.0.3"
|
mime_guess = "2.0.3"
|
||||||
mustache = "0.9.0"
|
pretty_env_logger = { version = "0.5" }
|
||||||
orizentic = { path = "../orizentic" }
|
|
||||||
params = "*"
|
|
||||||
router = "*"
|
|
||||||
serde_json = "*"
|
serde_json = "*"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
thiserror = "1.0.20"
|
thiserror = "1.0.20"
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
warp = { version = "0.3" }
|
warp = { version = "0.3" }
|
||||||
pretty_env_logger = { version = "0.5" }
|
|
||||||
log = { version = "0.4" }
|
[dev-dependencies]
|
||||||
bytes = { version = "1" }
|
|
||||||
futures-util = { version = "0.3" }
|
|
||||||
cool_asserts = { version = "2" }
|
cool_asserts = { version = "2" }
|
||||||
tempdir = { version = "0.3" }
|
tempdir = { version = "0.3" }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
use build_html::Html;
|
||||||
|
use http::{Error, StatusCode};
|
||||||
|
use std::{collections::HashMap, future::Future};
|
||||||
|
use warp::http::Response;
|
||||||
|
|
||||||
|
use crate::{pages, App, FileHandle, FileId, FileInfo, ReadFileError, SessionToken};
|
||||||
|
|
||||||
|
pub async fn handle_index(
|
||||||
|
app: App,
|
||||||
|
token: Option<SessionToken>,
|
||||||
|
) -> Result<Response<String>, Error> {
|
||||||
|
match token {
|
||||||
|
Some(token) => match app.auth_session(token).await {
|
||||||
|
Ok(_) => render_gallery_page(app).await,
|
||||||
|
Err(err) => render_auth_page(Some(format!("authentication refused: {:?}", err))),
|
||||||
|
},
|
||||||
|
None => render_auth_page(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(pages::auth(message).to_html_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_gallery_page(app: App) -> Result<Response<String>, Error> {
|
||||||
|
match app.list_files().await {
|
||||||
|
Ok(ids) => {
|
||||||
|
let mut files = vec![];
|
||||||
|
for id in ids.into_iter() {
|
||||||
|
let file = app.get_file(&id).await;
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
Response::builder()
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(pages::gallery(files).to_html_string())
|
||||||
|
}
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body("".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn thumbnail(
|
||||||
|
app: App,
|
||||||
|
id: String,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> Result<Response<Vec<u8>>, Error> {
|
||||||
|
match app.get_file(&FileId::from(id)).await {
|
||||||
|
Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
|
||||||
|
Err(_err) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn file(
|
||||||
|
app: App,
|
||||||
|
id: String,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> Result<Response<Vec<u8>>, Error> {
|
||||||
|
match app.get_file(&FileId::from(id)).await {
|
||||||
|
Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
|
||||||
|
Err(_err) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_auth(_form: HashMap<String, String>) -> Result<http::Response<String>, Error> {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NOT_IMPLEMENTED)
|
||||||
|
.body("".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_upload(
|
||||||
|
_app: App,
|
||||||
|
_token: SessionToken,
|
||||||
|
) -> Result<http::Response<String>, Error> {
|
||||||
|
println!("handle_upload");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NOT_IMPLEMENTED)
|
||||||
|
.body("".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_file<F>(
|
||||||
|
info: FileInfo,
|
||||||
|
file: F,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> http::Result<http::Response<Vec<u8>>>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<Vec<u8>, ReadFileError>,
|
||||||
|
{
|
||||||
|
match old_etags {
|
||||||
|
Some(old_etags) if old_etags != info.hash => Response::builder()
|
||||||
|
.header("content-type", info.file_type)
|
||||||
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
|
.body(vec![]),
|
||||||
|
_ => match file() {
|
||||||
|
Ok(content) => Response::builder()
|
||||||
|
.header("content-type", info.file_type)
|
||||||
|
.header("etag", info.hash)
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(content),
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(vec![]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,250 +1,44 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
|
use handlers::{file, handle_auth, handle_upload, thumbnail};
|
||||||
use http::status::StatusCode;
|
use http::status::StatusCode;
|
||||||
// use mustache::{compile_path, Template};
|
// use mustache::{compile_path, Template};
|
||||||
// use orizentic::{Permissions, ResourceName, Secret};
|
// use orizentic::{Permissions, ResourceName, Secret};
|
||||||
use build_html::Html;
|
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
convert::Infallible,
|
||||||
io::Read,
|
io::Read,
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use warp::{filters::multipart::Part, Filter};
|
use store::Username;
|
||||||
|
use warp::{filters::multipart::Part, Filter, Rejection};
|
||||||
|
|
||||||
// mod cookies;
|
mod handlers;
|
||||||
mod html;
|
mod html;
|
||||||
// mod middleware;
|
|
||||||
mod pages;
|
mod pages;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
pub use store::{FileHandle, FileId, FileInfo, ReadFileError, Store};
|
pub use handlers::handle_index;
|
||||||
|
pub use store::{App, AuthDB, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
|
async fn authenticate_user(app: App, auth_token: String) -> Result<Username, warp::Rejection> {
|
||||||
let Permissions(perms) = permissions;
|
match app.auth_session(SessionToken::from(auth_token)).await {
|
||||||
ResourceName(String::from(
|
Ok(username) => Ok(username),
|
||||||
"https://savanni.luminescent-dreams.com/file-service/",
|
Err(_) => Err(warp::reject::not_found()),
|
||||||
)) == *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(¤t_etag)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod files {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
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 serve_file<F>(
|
/*
|
||||||
info: FileInfo,
|
*/
|
||||||
file: F,
|
|
||||||
old_etags: Option<String>,
|
|
||||||
) -> http::Result<http::Response<Vec<u8>>>
|
|
||||||
where
|
|
||||||
F: FnOnce() -> Result<Vec<u8>, ReadFileError>,
|
|
||||||
{
|
|
||||||
match old_etags {
|
|
||||||
Some(old_etags) if old_etags != info.hash => warp::http::Response::builder()
|
|
||||||
.header("content-type", info.file_type)
|
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
|
||||||
.body(vec![]),
|
|
||||||
_ => match file() {
|
|
||||||
Ok(content) => warp::http::Response::builder()
|
|
||||||
.header("content-type", info.file_type)
|
|
||||||
.header("etag", info.hash)
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.body(content),
|
|
||||||
Err(_) => warp::http::Response::builder()
|
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
.body(vec![]),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
async fn collect_content(
|
async fn collect_content(
|
||||||
mut part: Part,
|
mut part: Part,
|
||||||
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
|
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
|
||||||
|
@ -261,7 +55,9 @@ async fn collect_content(
|
||||||
content,
|
content,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
async fn collect_multipart(
|
async fn collect_multipart(
|
||||||
mut stream: warp::filters::multipart::FormData,
|
mut stream: warp::filters::multipart::FormData,
|
||||||
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
|
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
|
||||||
|
@ -276,10 +72,12 @@ async fn collect_multipart(
|
||||||
|
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
async fn handle_upload(
|
async fn handle_upload(
|
||||||
form: warp::filters::multipart::FormData,
|
form: warp::filters::multipart::FormData,
|
||||||
app: Arc<RwLock<Store>>,
|
app: App,
|
||||||
) -> warp::http::Result<warp::http::Response<String>> {
|
) -> warp::http::Result<warp::http::Response<String>> {
|
||||||
let files = collect_multipart(form).await;
|
let files = collect_multipart(form).await;
|
||||||
match files {
|
match files {
|
||||||
|
@ -287,7 +85,7 @@ async fn handle_upload(
|
||||||
for (_, filename, content) in files {
|
for (_, filename, content) in files {
|
||||||
match filename {
|
match filename {
|
||||||
Some(filename) => {
|
Some(filename) => {
|
||||||
app.write().unwrap().add_file(filename, content).unwrap();
|
app.add_file(filename, content).unwrap();
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return warp::http::Response::builder()
|
return warp::http::Response::builder()
|
||||||
|
@ -310,150 +108,84 @@ async fn handle_upload(
|
||||||
.status(StatusCode::SEE_OTHER)
|
.status(StatusCode::SEE_OTHER)
|
||||||
.body("".to_owned())
|
.body("".to_owned())
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
fn with_app(app: App) -> impl Filter<Extract = (App,), Error = Infallible> + Clone {
|
||||||
|
warp::any().map(move || app.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error = Rejection> + Copy
|
||||||
|
{
|
||||||
|
warp::any()
|
||||||
|
.and(warp::header::optional::<String>("cookie"))
|
||||||
|
.map(|cookies| {
|
||||||
|
println!("cookies retrieved: {:?}", cookies);
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_session() -> impl Filter<Extract = (SessionToken,), Error = Rejection> + Copy {
|
||||||
|
warp::any()
|
||||||
|
.and(warp::header::<String>("cookie"))
|
||||||
|
.map(|token: String| SessionToken::from(token))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn 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 mut router = Router::new();
|
|
||||||
|
|
||||||
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();
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
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;
|
|
||||||
*/
|
|
||||||
|
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let app = Arc::new(RwLock::new(Store::new(PathBuf::from(
|
let authdb = AuthDB::new(PathBuf::from(":memory:")).await.unwrap();
|
||||||
&std::env::var("FILE_SHARE_DIR").unwrap(),
|
let store = Store::new(PathBuf::from(&std::env::var("FILE_SHARE_DIR").unwrap()));
|
||||||
))));
|
|
||||||
|
|
||||||
let app_filter = {
|
let app = App::new(authdb, store);
|
||||||
|
|
||||||
|
/*
|
||||||
|
let with_app = {
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
warp::any().map(move || app.clone())
|
warp::any().map(move || app.clone())
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
let log = warp::log("file_service");
|
let log = warp::log("file_service");
|
||||||
|
let root = warp::path!()
|
||||||
|
.and(warp::get())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.and(maybe_with_session())
|
||||||
|
.then(handle_index);
|
||||||
|
|
||||||
let root = warp::path!().and(warp::get()).and(app_filter.clone()).map({
|
let auth = warp::path!("auth")
|
||||||
move |app: Arc<RwLock<Store>>| {
|
|
||||||
info!("root handler");
|
|
||||||
let app = app.read().unwrap();
|
|
||||||
match app.list_files() {
|
|
||||||
Ok(ids) => {
|
|
||||||
let files = ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| app.get_file(&id))
|
|
||||||
.collect::<Vec<Result<FileHandle, ReadFileError>>>();
|
|
||||||
warp::http::Response::builder()
|
|
||||||
.header("content-type", "text/html")
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.body(pages::index(files).to_html_string())
|
|
||||||
}
|
|
||||||
Err(_) => warp::http::Response::builder()
|
|
||||||
.header("content-type", "text/html")
|
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
.body("".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let post_upload_handler = warp::path!("upload")
|
|
||||||
.and(warp::post())
|
|
||||||
.and(warp::filters::multipart::form().max_length(1024 * 1024 * 32))
|
|
||||||
.and(app_filter.clone())
|
|
||||||
.then(
|
|
||||||
|form: warp::filters::multipart::FormData, app: Arc<RwLock<Store>>| {
|
|
||||||
handle_upload(form, app)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
let post_delete_handler = warp::path!(String)
|
|
||||||
.and(warp::post())
|
.and(warp::post())
|
||||||
.and(warp::filters::body::form())
|
.and(warp::filters::body::form())
|
||||||
.map(|id: String, form: HashMap<String, String>| {
|
.then(handle_auth);
|
||||||
info!("post_delete {}", id);
|
|
||||||
info!("form: {:?}", form);
|
let upload_handler = warp::path!("upload")
|
||||||
warp::http::Response::builder()
|
.and(warp::post())
|
||||||
.header("location", "/")
|
.and(with_app(app.clone()))
|
||||||
.status(StatusCode::SEE_OTHER)
|
.and(with_session())
|
||||||
.body(vec![])
|
.then(handle_upload);
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
let thumbnail = warp::path!(String / "tn")
|
let thumbnail = warp::path!(String / "tn")
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(warp::header::optional::<String>("if-none-match"))
|
.and(warp::header::optional::<String>("if-none-match"))
|
||||||
.map({
|
.and(with_app(app.clone()))
|
||||||
let app = app.clone();
|
.then(move |id, old_etags, app: App| thumbnail(app, id, old_etags));
|
||||||
move |id: String, old_etags: Option<String>| match app
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.get_file(&FileId::from(id))
|
|
||||||
{
|
|
||||||
Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
|
|
||||||
Err(_err) => warp::http::Response::builder()
|
|
||||||
.status(StatusCode::NOT_FOUND)
|
|
||||||
.body(vec![]),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let file = warp::path!(String)
|
let file = warp::path!(String)
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(warp::header::optional::<String>("if-none-match"))
|
.and(warp::header::optional::<String>("if-none-match"))
|
||||||
.map({
|
.and(with_app(app.clone()))
|
||||||
let app = app.clone();
|
.then(move |id, old_etags, app: App| file(app, id, old_etags));
|
||||||
move |id: String, old_etags: Option<String>| match app
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.get_file(&FileId::from(id))
|
|
||||||
{
|
|
||||||
Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags),
|
|
||||||
Err(_err) => warp::http::Response::builder()
|
|
||||||
.status(StatusCode::NOT_FOUND)
|
|
||||||
.body(vec![]),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let server = warp::serve(
|
let server = warp::serve(
|
||||||
root.or(post_upload_handler)
|
root.or(auth).with(log), /*
|
||||||
|
root.or(auth)
|
||||||
.or(thumbnail)
|
.or(thumbnail)
|
||||||
.or(file)
|
.or(file)
|
||||||
|
.or(upload_handler)
|
||||||
.with(log),
|
.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;
|
||||||
|
|
|
@ -4,7 +4,22 @@ use crate::{
|
||||||
};
|
};
|
||||||
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
|
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
|
||||||
|
|
||||||
pub fn index(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
|
pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
|
||||||
|
build_html::HtmlPage::new()
|
||||||
|
.with_title("Authentication")
|
||||||
|
.with_html(
|
||||||
|
Form::new()
|
||||||
|
.with_path("/auth")
|
||||||
|
.with_method("post")
|
||||||
|
.with_container(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_html(Input::new("token", "token").with_id("for-token-input"))
|
||||||
|
.with_html(Label::new("for-token-input", "Authentication Token")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
|
||||||
let mut page = build_html::HtmlPage::new()
|
let mut page = build_html::HtmlPage::new()
|
||||||
.with_title("Admin list of files")
|
.with_title("Admin list of files")
|
||||||
.with_header(1, "Admin list of files")
|
.with_header(1, "Admin list of files")
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::sqlite::SqlitePool;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::{ops::Deref, path::PathBuf};
|
use std::{ops::Deref, path::PathBuf, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
mod filehandle;
|
mod filehandle;
|
||||||
mod fileinfo;
|
mod fileinfo;
|
||||||
|
@ -57,6 +59,82 @@ pub enum ReadFileError {
|
||||||
IOError(#[from] std::io::Error),
|
IOError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("database failed")]
|
||||||
|
SqlError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct Username(String);
|
||||||
|
|
||||||
|
impl From<String> for Username {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Username {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Username> for PathBuf {
|
||||||
|
fn from(s: Username) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Username> for PathBuf {
|
||||||
|
fn from(s: &Username) -> Self {
|
||||||
|
let Username(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Username {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct SessionToken(String);
|
||||||
|
|
||||||
|
impl From<String> for SessionToken {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for SessionToken {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SessionToken> for PathBuf {
|
||||||
|
fn from(s: SessionToken) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SessionToken> for PathBuf {
|
||||||
|
fn from(s: &SessionToken) -> Self {
|
||||||
|
let SessionToken(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SessionToken {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
pub struct FileId(String);
|
pub struct FileId(String);
|
||||||
|
|
||||||
|
@ -104,6 +182,63 @@ impl FileRoot for Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct App {
|
||||||
|
authdb: Arc<RwLock<AuthDB>>,
|
||||||
|
store: Arc<RwLock<Store>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(authdb: AuthDB, store: Store) -> Self {
|
||||||
|
Self {
|
||||||
|
authdb: Arc::new(RwLock::new(authdb)),
|
||||||
|
store: Arc::new(RwLock::new(store)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth_session(&self, token: SessionToken) -> Result<Username, AuthError> {
|
||||||
|
let authdb = self.authdb.read();
|
||||||
|
// authdb.auth_session(token).await
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
|
||||||
|
self.store.read().await.list_files()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
|
||||||
|
self.store.read().await.get_file(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_file(
|
||||||
|
&self,
|
||||||
|
filename: String,
|
||||||
|
content: Vec<u8>,
|
||||||
|
) -> Result<FileHandle, WriteFileError> {
|
||||||
|
self.store.write().await.add_file(filename, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthDB {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthDB {
|
||||||
|
pub async fn new(path: PathBuf) -> Result<Self, sqlx::Error> {
|
||||||
|
let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?;
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth_session(&self, _token: SessionToken) -> Result<Username, AuthError> {
|
||||||
|
/*
|
||||||
|
let conn = self.pool.acquire().await.map_err(|_| AuthError::SqlError)?;
|
||||||
|
conn.transaction(|tr| {})
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
files_root: PathBuf,
|
files_root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue