use std::fs::File; use std::io::Read; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::{env, fs}; use actix_files::NamedFile; use actix_web::error::ErrorInternalServerError; use actix_web::{error, get, http, middleware, web, App, Error, HttpResponse, HttpServer}; use chrono::{DateTime, Local}; use clap::{App as ClapApp, Arg}; use log::info; use percent_encoding::utf8_percent_encode; use pulldown_cmark::{html, Options, Parser}; use regex::{Captures, Regex}; use tera::{Context, Tera}; #[derive(Clone)] struct State { garden_dir: PathBuf, index_file: Option, title: Option, tera: Tera, } const VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() -> anyhow::Result<()> { let env = env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"); env_logger::init_from_env(env); let app = ClapApp::new("gardenserver") .version(VERSION) .author("Tomáš Mládek ") .arg(Arg::with_name("DIRECTORY").required(true).index(1)) .arg( Arg::with_name("BIND") .long("bind") .default_value("127.0.0.1:8642") .help("address and port to bind the Web interface on") .required(true), ) .arg( Arg::with_name("INDEX_FILE") .takes_value(true) .short("i") .long("index") .help("File to be served at the root."), ) .arg( Arg::with_name("TITLE") .takes_value(true) .short("t") .long("title") .help("Title of this digital garden."), ); let matches = app.get_matches(); let directory = Path::new(matches.value_of("DIRECTORY").unwrap()); info!( "Starting GardenServer {} of {}...", VERSION, directory.display() ); let tera = Tera::new("templates/**/*.html")?; let sys = actix::System::new("gardenserver"); let bind: SocketAddr = matches .value_of("BIND") .unwrap() .parse() .expect("Incorrect bind format."); info!("Starting server at: {}", &bind); let state = State { garden_dir: directory.to_path_buf(), index_file: matches.value_of("INDEX_FILE").map(|s| s.to_string()), title: matches.value_of("TITLE").map(|s| s.to_string()), tera, }; // Start HTTP server HttpServer::new(move || { App::new() .wrap(middleware::Logger::default()) .data(state.clone()) .service(render) }) .bind(&bind)? .run(); Ok(sys.run()?) } #[get("{path:.*}")] async fn render( request: web::HttpRequest, state: web::Data, path: web::Path, ) -> Result { let mut files: Vec = fs::read_dir(&state.garden_dir)? .filter_map(|entry| { if let Ok(entry) = entry { let path = entry.path(); if path.is_file() { return Some(path.strip_prefix(&state.garden_dir).unwrap().to_path_buf()); } } None }) .collect(); files.sort(); if files.is_empty() { return Err(error::ErrorNotFound("Garden is empty.")); } if path.is_empty() { let location = match state.index_file.as_ref() { Some(index_file) => index_file.clone(), None => files.get(0).unwrap().display().to_string(), }; return Ok(HttpResponse::Found() .header(http::header::LOCATION, location.as_str()) .finish()); } let full_path = state.garden_dir.join(path.as_str()); match (full_path.exists(), full_path.extension()) { (false, None) => { return Ok(HttpResponse::Found() .header(http::header::LOCATION, format!("{}.md", path.to_string())) .finish()); } (false, Some(_)) => return Err(error::ErrorNotFound("File not found.")), _ => {} } if !path.ends_with(".md") { Ok(NamedFile::open(full_path)?.into_response(&request)?) } else { let mut file = File::open(full_path.clone())?; let mut file_string = String::new(); file.read_to_string(&mut file_string)?; let markdown_source = preprocess(file_string); let parser = Parser::new_ext(markdown_source.as_str(), Options::all()); let mut html_output = String::new(); html::push_html(&mut html_output, parser); // TODO! let h1_regex = Regex::new(r"

([^>]+)

").unwrap(); let page_title = match h1_regex.captures(&html_output) { Some(h1_match) => h1_match.get(1).unwrap().as_str(), _ => full_path .components() .last() .unwrap() .as_os_str() .to_str() .unwrap_or("???"), }; let mtime: Option> = match file.metadata() { Ok(metadata) => metadata.modified().ok().map(|mtime| mtime.into()), _ => None, }; let mut context = Context::new(); context.insert( "garden_title", state .title .as_ref() .unwrap_or(&"Digital Garden".to_string()), ); context.insert("page_title", page_title); context.insert("files", &files); context.insert("content", &html_output); context.insert( "mtime", &mtime.map_or("???".to_string(), |t| t.format("%c").to_string()), ); Ok(HttpResponse::Ok().body( state .tera .render("main.html", &context) .map_err(ErrorInternalServerError)?, )) } } fn preprocess(string: String) -> String { let double_brackets = Regex::new(r"\[\[(?P[\w .]+)\]\]").unwrap(); double_brackets .replace_all(string.as_str(), |caps: &Captures| { format!( "[{}]({})", &caps[1], utf8_percent_encode(&caps[1], percent_encoding::NON_ALPHANUMERIC) ) }) .to_string() }