//! tagwiki use anyhow::{bail, format_err, Result}; use log::info; use std::sync::Arc; use structopt::StructOpt; use warp::{path::FullPath, Filter}; use serde_derive::Deserialize; use page::StoreMut; /// Command line options mod cli; /// Page mod page; mod index; /// Utils mod util; mod render; #[derive(Debug)] struct RejectAnyhow(anyhow::Error); impl warp::reject::Reject for RejectAnyhow {} /// Web-server shared state struct State { page_store: Arc>>>, } fn with_state( state: Arc, ) -> impl Filter,), Error = std::convert::Infallible> + Clone { warp::any().map(move || state.clone()) } fn warp_temporary_redirect(location: &str) -> warp::http::Response<&'static str> { warp::http::Response::builder() .status(307) .header(warp::http::header::LOCATION, location) .body("") .expect("correct redirect") } fn warp_temporary_redirect_to_get_method(location: &str) -> warp::http::Response<&'static str> { warp::http::Response::builder() .status(303) .header(warp::http::header::LOCATION, location) .body("") .expect("correct redirect") } fn get_rid_of_windows_newlines(s: String) -> String { s.chars().filter(|ch| *ch != '\r').collect() } #[derive(Deserialize, Debug)] struct GetParams { edit: Option, id: Option, q: Option, } #[derive(Deserialize, Debug)] struct PostForm { body: Option, id: Option, _method: Option, } impl PostForm { fn get_body(&self) -> Result<&str> { self.body .as_deref() .ok_or_else(|| format_err!("Missing body")) } } fn warp_reply_from_render(render: impl horrorshow::RenderOnce) -> Box { use horrorshow::Template; Box::new(warp::reply::html( render.into_string().expect("rendering without errors"), )) } fn path_to_tags(path: &FullPath) -> Vec<&str> { path.as_str() .split('/') .map(|t| t.trim()) .filter(|t| t != &"") .collect() } async fn handle_style_css() -> std::result::Result, warp::Rejection> { Ok(warp::http::Response::builder() .status(200) .header(warp::http::header::CONTENT_TYPE, "text/css") .body( // include_str!("../resources/reset.css").to_string() include_str!("../resources/style.css").to_string(), ) .expect("correct redirect")) } async fn handle_script_js() -> std::result::Result, warp::Rejection> { Ok(warp::http::Response::builder() .status(200) .header(warp::http::header::CONTENT_TYPE, "application/javascript") .body( include_str!("../resources/mousetrap.min.js").to_string() + include_str!("../resources/script.js"), ) .expect("correct response")) } async fn handle_query( query: GetParams, ) -> std::result::Result, warp::Rejection> { let q = "/".to_string() + &query .q .unwrap_or_else(|| String::new()) .split(" ") .filter(|s| !s.is_empty()) .collect::>() .join("/"); Ok(warp_temporary_redirect_to_get_method(&q)) } async fn handle_post_wrapped( state: Arc, path: FullPath, form: PostForm, ) -> Result, warp::Rejection> { if let Some("put") = form._method.as_deref() { // workaround for not being able to use `method="put"` in html forms handle_put(state, path, form) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } else if let Some("delete") = form._method.as_deref() { handle_delete(state, path, form) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } else { handle_post(state, path, form) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } } async fn handle_post( state: Arc, path: FullPath, form: PostForm, ) -> Result> { let tags = path_to_tags(&path); let mut write = state.page_store.write().await; let post_id = if let Some(id) = form.id.as_deref() { id.to_owned() } else { let results = write.find(tags.as_slice()); match results.matching_pages.len() { 1 => results.matching_pages[0].id.clone(), 0 => bail!("Page not found"), _ => return Ok(Box::new(warp_temporary_redirect_to_get_method(".".into()))), } }; let page = write.get(post_id.to_owned()).await?; let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.get_body()?.to_owned())); write.put(&page).await?; Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( "?id={}", post_id )))) } async fn handle_put_wrapped( state: Arc, path: FullPath, form: PostForm, ) -> Result, warp::Rejection> { handle_put(state, path, form) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } async fn handle_put( state: Arc, _path: FullPath, form: PostForm, ) -> Result> { let page = page::Parsed::new(&get_rid_of_windows_newlines(form.get_body()?.to_owned())); let mut write = state.page_store.write().await; write.put(&page).await?; Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( "?id={}", page.id() )))) } async fn handle_delete_wrapped( state: Arc, path: FullPath, form: PostForm, ) -> Result, warp::Rejection> { handle_delete(state, path, form) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } async fn handle_delete( state: Arc, _path: FullPath, query: PostForm, ) -> Result> { let mut write = state.page_store.write().await; let page = write .get(query.id.ok_or_else(|| format_err!("Missing ID"))?) .await?; write.delete(page.id().to_owned()).await?; Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( ".", )))) } // I wish this could be generic async fn handle_get_wrapped( state: Arc, path: FullPath, query: GetParams, ) -> std::result::Result, warp::Rejection> { handle_get(state, path, query) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } async fn handle_get( state: Arc, path: FullPath, query: GetParams, ) -> Result> { // rediect anything that does not end with `/` // This way relative links always work as expected. if !path.as_str().ends_with('/') { return Ok(Box::new(warp_temporary_redirect(&format!( "{}/", path.as_str() )))); } // because of this sucky mega-form, are getting here on enter // (which submits values of all inputs in the form) // to help with it - redirect to the right place, just like // if the user hit the "Search" button if let Some(q) = query.q.as_ref() { return Ok(Box::new(warp_temporary_redirect(&format!( "/_query?q={}", q )))); } let tags = path_to_tags(&path); let page_state = render::PageState { original_page_id: query.id.clone(), page: None, edit: query.edit.is_some(), path: path.as_str().to_string(), subtags: vec![], }; let read = state.page_store.read().await; let results = read.find(tags.as_slice()); if results.matching_tags != tags { return Ok(Box::new(warp_temporary_redirect( &("/".to_string() + &results.matching_tags.join("/")), ))); } let (page_id, subtags) = if query.edit.is_some() { (query.id, vec![]) } else if results.matching_pages.len() == 1 { (Some(results.matching_pages[0].id.clone()), vec![]) } else { let compact_results = read.compact_results(&results); if let Some(q_id) = query.id { (Some(q_id), compact_results.tags) } else if compact_results.direct_hit_pages.len() == 1 { ( Some(compact_results.direct_hit_pages[0].id.clone()), compact_results.tags, ) } else { return Ok(warp_reply_from_render(render::html_page( render::post_list( page_state, compact_results.tags.into_iter(), results.matching_pages.into_iter(), ), ))); } }; let page = if let Some(page_id) = page_id { Some(read.get(page_id).await?) } else { None }; Ok(warp_reply_from_render(render::html_page(render::page( render::PageState { page, subtags, ..page_state }, )))) } async fn start(opts: &cli::Opts) -> Result<()> { let state = Arc::new(State { page_store: Arc::new(tokio::sync::RwLock::new( index::Index::new(Box::new(page::store::FsStore::new(opts.path.clone())?) as Box) .await?, )), }); let handler = warp::any() .and(warp::path!("_style.css").and_then(handle_style_css)) .or(warp::path!("_script.js").and_then(handle_script_js)) .or(warp::path!("_query") .and(warp::query::()) .and_then(handle_query)) .or(with_state(state.clone()) .and(warp::path::full()) .and(warp::query::()) .and(warp::get()) .and_then(handle_get_wrapped)) .or(with_state(state.clone()) .and(warp::path::full()) .and(warp::post()) .and(warp::filters::body::form()) .and_then(handle_post_wrapped)) .or(with_state(state.clone()) .and(warp::path::full()) .and(warp::delete()) .and(warp::filters::body::form()) .and_then(handle_delete_wrapped)) .or(with_state(state) .and(warp::path::full()) .and(warp::put()) .and(warp::filters::body::form()) .and_then(handle_put_wrapped)); info!("Listening on port {}", opts.port); let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await; Ok(()) } fn main() -> Result<()> { env_logger::init(); let opts = cli::Opts::from_args(); tokio::runtime::Runtime::new() .unwrap() .block_on(start(&opts))?; Ok(()) } /* async fn handle_rejection( err: warp::Rejection, ) -> Result { use warp::http::StatusCode; let code; let message; if err.is_not_found() { code = StatusCode::NOT_FOUND; message = "NOT_FOUND"; } else if let Some(DivideByZero) = err.find() { code = StatusCode::BAD_REQUEST; message = "DIVIDE_BY_ZERO"; } else if let Some(_) = err.find::() { // We can handle a specific error, here METHOD_NOT_ALLOWED, // and render it however we want code = StatusCode::METHOD_NOT_ALLOWED; message = "METHOD_NOT_ALLOWED"; } else { // We should have expected this... Just log and say its a 500 eprintln!("unhandled rejection: {:?}", err); code = StatusCode::INTERNAL_SERVER_ERROR; message = "UNHANDLED_REJECTION"; } Ok(warp::reply::with_status(message, code)) }*/