Added rendering, removed route module, renamed io module

This commit is contained in:
Anthony J. Martinez 2021-04-04 21:03:22 +02:00
parent 55a07d2626
commit 7af4d8d42f
11 changed files with 184 additions and 136 deletions

7
Cargo.lock generated
View File

@ -148,6 +148,7 @@ dependencies = [
"argon2",
"clap",
"data-encoding",
"glob",
"log",
"pulldown-cmark",
"rand 0.8.3",
@ -386,6 +387,12 @@ dependencies = [
"wasi 0.10.2+wasi-snapshot-preview1",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "globset"
version = "0.4.6"

View File

@ -10,6 +10,7 @@ edition = "2018"
argon2 = "0.1"
clap = "2.33.3"
data-encoding = "2.3.2"
glob = "0.3.0"
log = "0.4.14"
pulldown-cmark = { version = "0.8", default-features = false, features = ["simd"] }
rand = "0.8"

View File

@ -1,9 +1,12 @@
use std::sync::Arc;
use caty_blog::*;
use warp::Filter;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = config::load()?;
println!("{:?}", config);
let app = Arc::new(config);
// Configure logging
@ -11,6 +14,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// - Either generate the base directory structure for a blog
// - Or load an existing site and serve its routes based on the loaded config
// warp::serve(routes).run(([127,0,0,1], 3030)).await;
Ok(())
}

View File

@ -1,6 +1,8 @@
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::Path;
use std::path::{Path, PathBuf};
use glob::glob;
#[cfg(target_family = "unix")]
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
@ -29,3 +31,22 @@ pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<
perms.set_readonly(true);
Ok(())
}
pub fn path_matches(pat: &str) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
let mut path_vec: Vec<PathBuf> = Vec::new();
let entries = glob(pat)?;
for entry in entries.filter_map(Result::ok) {
path_vec.push(entry);
}
path_vec.reverse();
Ok(path_vec)
}
pub fn slugify(topic: &str) -> String {
let topic = topic
.to_ascii_lowercase()
.replace(char::is_whitespace, "-");
topic
}

View File

@ -8,6 +8,7 @@ use serde::{Serialize, Deserialize};
use toml;
use crate::auth;
use crate::common;
fn args() -> App<'static, 'static> {
App::new("Caty's Blog")
@ -112,7 +113,7 @@ impl Credentials {
const PASSWORD_LEN: usize = 32;
let password = auth::generate_secret(PASSWORD_LEN)?;
let password_file = dir.as_ref().join("admin.pass");
crate::io::str_to_ro_file(&password, password_file)?;
common::str_to_ro_file(&password, password_file)?;
let password = auth::generate_argon2_phc(&password)?;
const TOKEN_LEN: usize = 34;
@ -120,7 +121,7 @@ impl Credentials {
let token = token.as_bytes();
let token = auth::BASE32_NOPAD.encode(token);
let token_file = dir.as_ref().join("admin.totp");
crate::io::str_to_ro_file(&token, token_file)?;
common::str_to_ro_file(&token, token_file)?;
let creds = Credentials { user, password, token };
@ -196,9 +197,7 @@ impl AppConfig {
create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;
for topic in &self.blog.topics {
let topic = topic
.to_ascii_lowercase()
.replace(char::is_whitespace, "-");
let topic = common::slugify(&topic);
create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
create_dir_all(format!("{}/{}/posts", &self.docpaths.webroot, &topic))?;
@ -209,7 +208,7 @@ impl AppConfig {
fn write<P: AsRef<Path>>(&self, dir: P) -> Result<(), Box<dyn std::error::Error>> {
let config = toml::to_string_pretty(&self)?;
let conf_path = &dir.as_ref().join("config.toml");
crate::io::str_to_ro_file(&config, &conf_path)?;
common::str_to_ro_file(&config, &conf_path)?;
Ok(())
}
}

View File

@ -1,4 +1,4 @@
pub mod auth;
pub mod config;
pub mod io;
pub mod server;
pub mod common;
pub mod render;

110
src/render.rs Normal file
View File

@ -0,0 +1,110 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::config::AppConfig;
use crate::common;
pub use pulldown_cmark::{Parser, html};
pub use tera::{Tera, Context};
mod default;
pub struct Engine {
pub app: Arc<AppConfig>,
pub topic_slug: String,
pub template: String,
pub instance: Tera
}
impl Engine {
pub fn new(a: Arc<AppConfig>, ts: &str, tmpl: &str, inst: Tera) -> Engine {
Engine {
app: a.clone(),
topic_slug: ts.to_string(),
template: tmpl.to_string(),
instance: inst
}
}
}
pub fn load_default_template() -> Result<Tera, Box<dyn std::error::Error>> {
let mut tera = Tera::default();
tera.add_raw_template("default.tmpl", default::TEMPLATE)?;
Ok(tera)
}
pub fn render_topic(engine: Engine) -> Result<String, Box<dyn std::error::Error>> {
let blog = &engine.app.blog;
let topic_data = load_topic(&engine)?;
let mut context = Context::new();
context.insert("blog", blog);
context.insert("posts", &topic_data);
let output = engine.instance.render(&engine.template, &context)?;
Ok(output)
}
fn load_topic(engine: &Engine) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let topic_path = Path::new(&engine.app.docpaths.webroot).join(&engine.topic_slug);
let pat = format!("{}/*.md", topic_path.display());
let paths = common::path_matches(&pat)?;
read_to_html(paths)
}
fn read_to_html(paths: Vec<PathBuf>) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut contents: Vec<String> = Vec::new();
for path in paths {
let buf = std::fs::read_to_string(path)?;
let parser = Parser::new(&buf);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
contents.push(html_output);
}
Ok(contents)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::prelude::*;
use tempfile;
#[test]
fn check_default_template() {
let tera = load_default_template();
assert!(tera.is_ok())
}
#[test]
fn check_render_topic() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let instance = load_default_template().unwrap();
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config, "one", "default.tmpl", instance);
let post = r#"
### Something
Very cool, but maybe not super useful
"#;
let post2 = r#"
### Something Again
Super Wow!
"#;
let mut f = File::create(&dir.path().join("blog").join("webroot").join("one").join("post1.md")).unwrap();
f.write_all(&post.as_bytes()).unwrap();
let mut f = File::create(&dir.path().join("blog").join("webroot").join("one").join("post2.md")).unwrap();
f.write_all(&post2.as_bytes()).unwrap();
let page = render_topic(engine);
assert!(page.is_ok())
}
}

32
src/render/default.rs Normal file
View File

@ -0,0 +1,32 @@
pub const TEMPLATE: &str = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<title>{{ blog.name }}</title>
</head>
<body>
<header>
<center>
<h1>{{ blog.name }}</h1>
<nav>
<a href="./">HOME</a>
{%- for topic in blog.topics %}
<a href="./{{ topic | slugify }}">{{ topic }}</a>
{%- endfor -%}
</nav>
</center>
</header>
<main>
{%- for post in posts %}
{{ post }}
{%- endfor -%}
</main>
<footer>
<p>&#169; {{ blog.author }}</p>
</footer>
</body>
</html>
"#;

View File

@ -1,2 +0,0 @@
pub mod routes;
pub mod render;

View File

@ -1,11 +0,0 @@
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use pulldown_cmark::{Parser, html};
use tera::{Tera, Context};
// Placeholder for template and markdown rendering
#[cfg(test)]
mod tests {
}

View File

@ -1,112 +0,0 @@
use std::path::Path;
use std::sync::Arc;
use warp::{Filter, Reply, filters::BoxedFilter};
use crate::config::AppConfig;
// Placeholder for Warp routes
/*
fn root(app: Arc<AppConfig>) -> BoxedFilter<(impl Reply,)> {
let app = &app.clone();
let main_path = Path::new(&app.docpaths.webroot).join("main");
}
*/
fn static_assets(app: Arc<AppConfig>) -> BoxedFilter<(impl Reply,)> {
let app = &app.clone();
let static_path = Path::new(&app.docpaths.webroot).join("static");
warp::path("static")
.and(warp::fs::dir(static_path))
.boxed()
}
fn topic_assets(app: Arc<AppConfig>, topic: &'static str) -> BoxedFilter<(impl Reply,)> {
let app = &app.clone();
let topic_asset_path = Path::new(&app.docpaths.webroot).join(topic).join("ext");
warp::path(topic)
.and(warp::path("ext"))
.and(warp::fs::dir(topic_asset_path))
.boxed()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AppConfig;
use tempfile;
/*
#[tokio::test]
async fn main_page() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let filter = root(config);
assert!(!warp::test::request()
.path("/")
.matches(&filter)
.await);
}
*/
#[tokio::test]
async fn static_content() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let filter = static_assets(config);
assert!(!warp::test::request()
.path("/static/style.css")
.matches(&filter)
.await);
}
/*
#[tokio::test]
async fn topic_page() {
let filter = topic();
}
*/
#[tokio::test]
async fn topic_content() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let filter = topic_assets(config.clone(), "one");
let filter2 = topic_assets(config.clone(), "two");
let filter3 = topic_assets(config.clone(), "three");
let filter_and_more = topic_assets(config.clone(), "and-more");
assert!(!warp::test::request()
.path("/one/ext/example.png")
.matches(&filter)
.await);
assert!(!warp::test::request()
.path("/two/ext/example.png")
.matches(&filter2)
.await);
assert!(!warp::test::request()
.path("/three/ext/example.png")
.matches(&filter3)
.await);
assert!(!warp::test::request()
.path("/and-more/ext/example.png")
.matches(&filter_and_more)
.await);
}
}