Added rendering, removed route module, renamed io module
This commit is contained in:
parent
55a07d2626
commit
7af4d8d42f
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod io;
|
||||
pub mod server;
|
||||
pub mod common;
|
||||
pub mod render;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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>© {{ blog.author }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
|
@ -1,2 +0,0 @@
|
|||
pub mod routes;
|
||||
pub mod render;
|
|
@ -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 {
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue