use build_html::Html; use bytes::Buf; use file_service::WriteFileError; use futures_util::StreamExt; use http::{Error, StatusCode}; use std::collections::HashMap; use std::io::Read; use warp::{filters::multipart::FormData, http::Response, multipart::Part}; use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; const CSS: &str = include_str!("../templates/style.css"); pub async fn handle_index( app: App, token: Option, ) -> Result, Error> { match token { Some(token) => match app.validate_session(token).await { Ok(_) => render_gallery_page(app).await, Err(err) => render_auth_page(Some(format!("session expired: {:?}", err))), }, None => render_auth_page(None), } } pub async fn handle_css() -> Result, Error> { Response::builder() .header("content-type", "text/css") .status(StatusCode::OK) .body(CSS.to_owned()) } pub fn render_auth_page(message: Option) -> Result, Error> { Response::builder() .status(StatusCode::OK) .body(pages::auth(message).to_html_string()) } pub async fn render_gallery_page(app: App) -> Result, 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, ) -> Result>, 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, ) -> Result>, Error> { match app.get_file(&FileId::from(id)).await { Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags), Err(_err) => Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), } } pub async fn handle_auth( app: App, form: HashMap, ) -> Result, Error> { match form.get("token") { Some(token) => match app.authenticate(AuthToken::from(token.clone())).await { Ok(Some(session_token)) => Response::builder() .header("location", "/") .header( "set-cookie", format!( "session={}; Secure; HttpOnly; SameSite=Strict", *session_token ), ) .status(StatusCode::SEE_OTHER) .body("".to_owned()), Ok(None) => render_auth_page(Some("no user found".to_owned())), Err(_) => render_auth_page(Some("invalid auth token".to_owned())), }, None => render_auth_page(Some("no token available".to_owned())), } } pub async fn handle_upload( app: App, token: SessionToken, form: FormData, ) -> Result, Error> { match app.validate_session(token).await { Ok(Some(_)) => match process_file_upload(app, form).await { Ok(_) => Response::builder() .header("location", "/") .status(StatusCode::SEE_OTHER) .body("".to_owned()), Err(UploadError::FilenameMissing) => Response::builder() .status(StatusCode::BAD_REQUEST) .body("filename is required for all files".to_owned()), Err(UploadError::WriteFileError(err)) => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(format!("could not write to the file system: {:?}", err)), Err(UploadError::WarpError(err)) => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(format!("error with the app framework: {:?}", err)), }, _ => Response::builder() .status(StatusCode::UNAUTHORIZED) .body("".to_owned()), } } fn serve_file( info: FileInfo, file: F, old_etags: Option, ) -> http::Result>> where F: FnOnce() -> Result, 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![]), }, } } async fn collect_multipart( mut stream: warp::filters::multipart::FormData, ) -> Result, Option, Vec)>, warp::Error> { let mut content: Vec<(Option, Option, Vec)> = Vec::new(); while let Some(part) = stream.next().await { match part { Ok(part) => content.push(collect_content(part).await.unwrap()), Err(err) => return Err(err), } } Ok(content) } async fn collect_content( mut part: Part, ) -> Result<(Option, Option, Vec), String> { let mut content: Vec = Vec::new(); while let Some(Ok(data)) = part.data().await { let mut reader = data.reader(); reader.read_to_end(&mut content).unwrap(); } Ok(( part.content_type().map(|s| s.to_owned()), part.filename().map(|s| s.to_owned()), content, )) } /* async fn handle_upload( form: warp::filters::multipart::FormData, app: App, ) -> warp::http::Result> { let files = collect_multipart(form).await; match files { Ok(files) => { for (_, filename, content) in files { match filename { Some(filename) => { app.add_file(filename, content).unwrap(); } None => { return warp::http::Response::builder() .status(StatusCode::BAD_REQUEST) .body("".to_owned()) } } } } Err(_err) => { return warp::http::Response::builder() .status(StatusCode::BAD_REQUEST) .body("".to_owned()) } } // println!("file length: {:?}", files.map(|f| f.len())); warp::http::Response::builder() .header("location", "/") .status(StatusCode::SEE_OTHER) .body("".to_owned()) } */ enum UploadError { FilenameMissing, WriteFileError(WriteFileError), WarpError(warp::Error), } impl From for UploadError { fn from(err: WriteFileError) -> Self { Self::WriteFileError(err) } } impl From for UploadError { fn from(err: warp::Error) -> Self { Self::WarpError(err) } } async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> { let files = collect_multipart(form).await?; for (_, filename, content) in files { match filename { Some(filename) => { app.add_file(filename, content).await?; } None => return Err(UploadError::FilenameMissing), } } Ok(()) }