2020-10-09 21:02:45 +02:00
|
|
|
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};
|
2020-10-09 22:45:29 +02:00
|
|
|
use linkify::LinkFinder;
|
2020-10-09 21:02:45 +02:00
|
|
|
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())
|
2020-10-11 14:40:14 +02:00
|
|
|
.service(actix_files::Files::new("/static", "templates"))
|
2020-10-09 21:02:45 +02:00
|
|
|
.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() {
|
2020-10-20 19:31:00 +02:00
|
|
|
let stripped_path = path.strip_prefix(&state.garden_dir).unwrap().to_path_buf();
|
|
|
|
if !stripped_path.to_str().unwrap().starts_with(".") {
|
|
|
|
return Some(stripped_path);
|
|
|
|
}
|
2020-10-09 21:02:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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(),
|
2020-10-20 19:13:51 +02:00
|
|
|
None => files
|
|
|
|
.iter()
|
|
|
|
.filter(|f| f.to_str().unwrap().ends_with(".md"))
|
|
|
|
.collect::<Vec<&PathBuf>>()
|
|
|
|
.first()
|
|
|
|
.unwrap_or(&files.first().unwrap())
|
|
|
|
.display()
|
|
|
|
.to_string(),
|
2020-10-09 21:02:45 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
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()),
|
|
|
|
);
|
2020-10-09 23:00:15 +02:00
|
|
|
context.insert("version", VERSION);
|
2020-10-09 21:02:45 +02:00
|
|
|
|
|
|
|
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();
|
2020-10-09 22:45:29 +02:00
|
|
|
let finder = LinkFinder::new();
|
2020-10-09 21:02:45 +02:00
|
|
|
|
2020-10-09 22:45:29 +02:00
|
|
|
let result = double_brackets
|
2020-10-09 21:02:45 +02:00
|
|
|
.replace_all(string.as_str(), |caps: &Captures| {
|
|
|
|
format!(
|
|
|
|
"[{}]({})",
|
|
|
|
&caps[1],
|
|
|
|
utf8_percent_encode(&caps[1], percent_encoding::NON_ALPHANUMERIC)
|
|
|
|
)
|
|
|
|
})
|
2020-10-09 22:45:29 +02:00
|
|
|
.to_string();
|
|
|
|
|
|
|
|
let result_vec = Vec::from(result.as_str());
|
|
|
|
let start_delims = vec![b'(', b'<'];
|
|
|
|
let end_delims = vec![b')', b'>'];
|
2020-10-12 21:46:28 +02:00
|
|
|
// link.end() is the first char AFTER the link!
|
2020-10-09 22:45:29 +02:00
|
|
|
let links = finder.links(result.as_str()).filter(|link| {
|
2020-10-12 21:46:28 +02:00
|
|
|
link.start() == 0
|
|
|
|
|| link.end() == result.len()
|
|
|
|
|| !start_delims.contains(&result_vec[link.start() - 1])
|
|
|
|
|| !end_delims.contains(&result_vec[link.end()])
|
2020-10-09 22:45:29 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
let mut offset = 0;
|
|
|
|
let mut result_string = result.to_string();
|
|
|
|
for link in links {
|
|
|
|
let orig = link.as_str();
|
|
|
|
let new = format!("<{}>", orig);
|
|
|
|
result_string.replace_range((link.start() + offset)..(link.end() + offset), new.as_str());
|
|
|
|
offset += new.len() - orig.len();
|
|
|
|
}
|
|
|
|
|
|
|
|
result_string
|
2020-10-09 21:02:45 +02:00
|
|
|
}
|