gardenserver/src/main.rs

211 lines
6.2 KiB
Rust

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<String>,
title: Option<String>,
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 <t@mldk.cz>")
.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<State>,
path: web::Path<String>,
) -> Result<HttpResponse, Error> {
let mut files: Vec<PathBuf> = 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"<h1>([^>]+)</h1>").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<DateTime<Local>> = 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<inner>[\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()
}