280 lines
8.6 KiB
Rust
280 lines
8.6 KiB
Rust
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<SessionToken>,
|
|
) -> Result<Response<String>, 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<Response<String>, Error> {
|
|
Response::builder()
|
|
.header("content-type", "text/css")
|
|
.status(StatusCode::OK)
|
|
.body(CSS.to_owned())
|
|
}
|
|
|
|
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.content(), old_etags),
|
|
Err(_err) => Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(vec![]),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_auth(
|
|
app: App,
|
|
form: HashMap<String, String>,
|
|
) -> Result<http::Response<String>, Error> {
|
|
match form.get("password") {
|
|
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<http::Response<String>, 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()),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_delete(
|
|
app: App,
|
|
token: SessionToken,
|
|
id: FileId,
|
|
) -> Result<http::Response<String>, Error> {
|
|
match app.validate_session(token).await {
|
|
Ok(Some(_)) => match app.delete_file(id).await {
|
|
Ok(_) => Response::builder()
|
|
.header("location", "/")
|
|
.status(StatusCode::SEE_OTHER)
|
|
.body("".to_owned()),
|
|
Err(_) => unimplemented!(),
|
|
},
|
|
_ => Response::builder()
|
|
.status(StatusCode::UNAUTHORIZED)
|
|
.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![]),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn collect_multipart(
|
|
mut stream: warp::filters::multipart::FormData,
|
|
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
|
|
let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = 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<String>, Option<String>, Vec<u8>), String> {
|
|
let mut content: Vec<u8> = 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<warp::http::Response<String>> {
|
|
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<WriteFileError> for UploadError {
|
|
fn from(err: WriteFileError) -> Self {
|
|
Self::WriteFileError(err)
|
|
}
|
|
}
|
|
|
|
impl From<warp::Error> 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(())
|
|
}
|