Merge pull request 'Add crate logging' (#17) from logging into main
Reviewed-on: #17
This commit is contained in:
commit
23d30c9171
|
@ -30,7 +30,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "arse"
|
||||
version = "0.2.0-alpha.1"
|
||||
version = "0.3.0-alpha.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"clap",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "arse"
|
||||
version = "0.2.0-alpha.1"
|
||||
version = "0.3.0-alpha.1"
|
||||
authors = ["Anthony Martinez"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
@ -19,7 +19,7 @@ clap = "2.33.3"
|
|||
data-encoding = "2.3.2"
|
||||
glob = "0.3.0"
|
||||
hyper = "0.14"
|
||||
log = "0.4.14"
|
||||
log = "0.4"
|
||||
pulldown-cmark = { version = "0.8", default-features = false, features = ["simd"] }
|
||||
rand = "0.8"
|
||||
routerify = "2.0.0"
|
||||
|
|
11
README.md
11
README.md
|
@ -6,11 +6,13 @@ and flexible base for serving sites using:
|
|||
* [Tera](https://tera.netlify.app/) for templates
|
||||
* [pulldown-cmark](https://crates.io/crates/pulldown-cmark) for CommonMark rendering
|
||||
* [routerify](https://crates.io/crates/routerify) to serve the site
|
||||
* [simplecss](https://simplecss.org) for default styling
|
||||
|
||||
## Binary
|
||||
|
||||
* Run an existing site given the path to its config TOML: `arse run config.toml`
|
||||
* Create and run a new site from user input: `arse new`
|
||||
* Logging verbosity can be increased with `-v` or `-vv`, the default level is `INFO`.
|
||||
|
||||
## Library
|
||||
|
||||
|
@ -18,6 +20,15 @@ and flexible base for serving sites using:
|
|||
|
||||
Documentation can be found [here](https://docs.rs/arse/).
|
||||
|
||||
## Path to 1.0
|
||||
|
||||
- [x] Dynamic route handling
|
||||
- [x] Provide meaningful logging of library and binary activites at appropriate levels
|
||||
- [ ] Documentation of full public API
|
||||
- [ ] Support custom Tera templates
|
||||
- [ ] Context-specific Errors and handling
|
||||
- [ ] Administration portal for site management
|
||||
|
||||
### License
|
||||
|
||||
Licensed under either of
|
||||
|
|
77
src/auth.rs
77
src/auth.rs
|
@ -1,41 +1,56 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
/// TODO Document
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use log::{debug, error};
|
||||
|
||||
/**
|
||||
TODO Document
|
||||
*/
|
||||
pub fn generate_secret(len: usize) -> Result<String, Box<dyn std::error::Error>> {
|
||||
use rand::{thread_rng, Rng,
|
||||
distributions::Alphanumeric};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
if len < 32 {
|
||||
Err(From::from("Random passwords shorter than 32ch are useless"))
|
||||
error!("Attempting to use password < 32ch.");
|
||||
Err(From::from("Random passwords shorter than 32ch are useless"))
|
||||
} else {
|
||||
let pass: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(len)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
Ok(pass)
|
||||
let pass: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(len)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
debug!("New secret generated");
|
||||
Ok(pass)
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO Document
|
||||
/**
|
||||
TODO Document
|
||||
*/
|
||||
pub fn generate_argon2_phc(secret: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
use argon2::{
|
||||
password_hash::{PasswordHasher, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use argon2::{Argon2, password_hash::{SaltString, PasswordHasher}};
|
||||
|
||||
let secret = secret.as_bytes();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let argon2_phc: Result<String, Box<dyn std::error::Error>>;
|
||||
|
||||
if let Ok(phc) = argon2.hash_password_simple(secret, salt.as_ref()) {
|
||||
argon2_phc = Ok(phc.to_string());
|
||||
debug!("Created Argon2 PHC string");
|
||||
argon2_phc = Ok(phc.to_string());
|
||||
} else {
|
||||
argon2_phc = Err(From::from("Failed to hash password"));
|
||||
error!("Failed to create Argon2 PHC");
|
||||
argon2_phc = Err(From::from("Failed to hash password"));
|
||||
}
|
||||
|
||||
argon2_phc
|
||||
|
@ -49,23 +64,23 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn check_secret_len() {
|
||||
const SECLEN: usize = 32;
|
||||
let secret = generate_secret(SECLEN).unwrap();
|
||||
assert_eq!(SECLEN, secret.len())
|
||||
const SECLEN: usize = 32;
|
||||
let secret = generate_secret(SECLEN).unwrap();
|
||||
assert_eq!(SECLEN, secret.len())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_short_secret() {
|
||||
const SECLEN: usize = 12;
|
||||
let secret = generate_secret(SECLEN);
|
||||
assert!(secret.is_err())
|
||||
const SECLEN: usize = 12;
|
||||
let secret = generate_secret(SECLEN);
|
||||
assert!(secret.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_argon2_hasher() {
|
||||
const SECLEN: usize = 32;
|
||||
let secret = generate_secret(SECLEN).unwrap();
|
||||
let phc = generate_argon2_phc(&secret);
|
||||
assert!(phc.is_ok())
|
||||
const SECLEN: usize = 32;
|
||||
let secret = generate_secret(SECLEN).unwrap();
|
||||
let phc = generate_argon2_phc(&secret);
|
||||
assert!(phc.is_ok())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arse::*;
|
||||
use log::{info, error};
|
||||
use hyper::Server;
|
||||
use routerify::RouterService;
|
||||
|
||||
|
@ -17,16 +20,22 @@ use routerify::RouterService;
|
|||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = config::load()?;
|
||||
let app = Arc::new(config);
|
||||
info!("Configuration loaded");
|
||||
|
||||
// TODO: Configure logging
|
||||
|
||||
let router = routes::router(app.clone());
|
||||
info!("Route handlers loaded");
|
||||
|
||||
let service = RouterService::new(router).unwrap();
|
||||
info!("Service loaded");
|
||||
|
||||
let addr: SocketAddr = "0.0.0.0:9090".parse().unwrap();
|
||||
|
||||
info!("Creating server on: {}", &addr);
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
|
||||
info!("Running server on: {}", &addr);
|
||||
if let Err(err) = server.await {
|
||||
eprintln!("Server error: {}", err)
|
||||
error!("Server error: {}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,60 +1,75 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use glob::glob;
|
||||
use log::{debug, trace};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
debug!("Writing protected file: {}", &dest.as_ref().display());
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true);
|
||||
options.write(true);
|
||||
options.mode(0o600);
|
||||
|
||||
trace!("Opening '{}' to write", &dest.as_ref().display());
|
||||
let mut ro_file = options.open(dest)?;
|
||||
ro_file.write_all(content.as_bytes())?;
|
||||
if !content.ends_with("\n") {
|
||||
ro_file.write(b"\n")?;
|
||||
if !content.ends_with('\n') {
|
||||
ro_file.write_all(b"\n")?;
|
||||
}
|
||||
trace!("Content written to destination");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
debug!("Writing protected file: {}", &dest.as_ref().display());
|
||||
trace!("Opening '{}' to write", &dest.as_ref().display());
|
||||
let mut ro_file = File::create(dest)?;
|
||||
ro_file.write_all(content.as_bytes())?;
|
||||
let metadata = secret_file.metadata()?;
|
||||
let mut perms = metadata.permissions();
|
||||
if !content.ends_with("\n") {
|
||||
ro_file.write(b"\n")?;
|
||||
if !content.ends_with('\n') {
|
||||
ro_file.write_all(b"\n")?;
|
||||
}
|
||||
trace!("Content written to destination");
|
||||
trace!("Setting read-only on destination file");
|
||||
perms.set_readonly(true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn path_matches(pat: &str) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
|
||||
debug!("Building topic content vector from {}", &pat);
|
||||
let mut path_vec: Vec<PathBuf> = Vec::new();
|
||||
|
||||
trace!("Globbing {}", &pat);
|
||||
let entries = glob(pat)?;
|
||||
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
trace!("Adding '{}' to topic content vector", &entry.display());
|
||||
path_vec.push(entry);
|
||||
}
|
||||
|
||||
trace!("Reversing topic content vector for LIFO site rendering");
|
||||
path_vec.reverse();
|
||||
Ok(path_vec)
|
||||
}
|
||||
|
||||
pub fn slugify(topic: &str) -> String {
|
||||
let topic = topic
|
||||
.to_ascii_lowercase()
|
||||
.replace(char::is_whitespace, "-");
|
||||
topic
|
||||
debug!("Creating slugified topic string from {}", &topic);
|
||||
topic.to_ascii_lowercase().replace(char::is_whitespace, "-")
|
||||
}
|
||||
|
||||
|
|
165
src/config.rs
165
src/config.rs
|
@ -1,54 +1,80 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::{io::BufRead, usize};
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand, crate_authors, crate_description, crate_version};
|
||||
use log::{debug, error, info, trace};
|
||||
use simplelog::{SimpleLogger, ConfigBuilder};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use toml;
|
||||
|
||||
use super::auth;
|
||||
use super::common;
|
||||
|
||||
fn args() -> App<'static, 'static> {
|
||||
App::new("Caty's Blog")
|
||||
.version("1.0")
|
||||
.author("Anthony Martinez")
|
||||
App::new("A Rust Site Engine")
|
||||
.version(crate_version!())
|
||||
.author(crate_authors!())
|
||||
.about(crate_description!())
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.arg(Arg::with_name("verbosity")
|
||||
.short("v")
|
||||
.multiple(true)
|
||||
.help("Sets the log level. Default: INFO. -v = DEBUG, -vv = TRACE"))
|
||||
.subcommand(SubCommand::with_name("run")
|
||||
.about("Run the blog server")
|
||||
.about("Run the site server")
|
||||
.arg(Arg::with_name("config")
|
||||
.help("Provides the path to the server configuration file.")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.index(1)))
|
||||
.subcommand(SubCommand::with_name("new")
|
||||
.about("Generates a base directory structure and configuration file for a new blog")
|
||||
.about("Generates a base directory structure and configuration file for a new site")
|
||||
)
|
||||
}
|
||||
|
||||
/// TODO Document this public function
|
||||
/// And Include an Example of its Use
|
||||
pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
|
||||
let matches = args().get_matches();
|
||||
let config: Result<AppConfig, Box<dyn std::error::Error>>;
|
||||
let matches = args().get_matches();
|
||||
|
||||
// Create a Config with ISO timestamps
|
||||
let log_config = ConfigBuilder::new()
|
||||
.set_time_format_str("%+")
|
||||
.build();
|
||||
|
||||
// After this block locking is configured at the specified level
|
||||
match matches.occurrences_of("verbosity") {
|
||||
0 => SimpleLogger::init(log::LevelFilter::Info, log_config)?,
|
||||
1 => SimpleLogger::init(log::LevelFilter::Debug, log_config)?,
|
||||
_ => SimpleLogger::init(log::LevelFilter::Trace, log_config)?,
|
||||
}
|
||||
|
||||
info!("Logging started");
|
||||
|
||||
debug!("Processing subcommands");
|
||||
if matches.is_present("run") {
|
||||
trace!("Application called with `run` subcommand - loading config from disk");
|
||||
config = runner_config(matches);
|
||||
} else if matches.is_present("new") {
|
||||
trace!("Application called with `new` subcommand - creating config from user input");
|
||||
let reader = std::io::stdin();
|
||||
let mut reader = reader.lock();
|
||||
let current_path = std::env::current_dir()?;
|
||||
config = AppConfig::generate(current_path, &mut reader);
|
||||
} else {
|
||||
let msg = format!("Unable to load configuration");
|
||||
let msg = "Unable to load configuration".to_owned();
|
||||
error!("{}", &msg);
|
||||
config = Err(From::from(msg));
|
||||
}
|
||||
|
||||
|
@ -58,10 +84,11 @@ pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
|
|||
fn runner_config(m: ArgMatches) -> Result<AppConfig, Box<dyn std::error::Error>> {
|
||||
if let Some(run) = m.subcommand_matches("run") {
|
||||
let value = run.value_of("config").unwrap();
|
||||
let config = AppConfig::new(value)?;
|
||||
let config = AppConfig::from_path(value)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
let msg = format!("Failed to read arguments for 'run' subcommand");
|
||||
let msg = "Failed to read arguments for 'run' subcommand".to_owned();
|
||||
error!("{}", &msg);
|
||||
Err(From::from(msg))
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +104,8 @@ fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String, Box<dyn
|
|||
}
|
||||
|
||||
fn csv_to_vec(csv: &str) -> Vec<String> {
|
||||
let val_vec: Vec<String> = csv.split(",")
|
||||
debug!("Creating Vec<String> from csv topics: {}", &csv);
|
||||
let val_vec: Vec<String> = csv.split(',')
|
||||
.map(|s| s
|
||||
.trim_start_matches(char::is_whitespace)
|
||||
.trim_end_matches(char::is_whitespace)
|
||||
|
@ -89,21 +117,22 @@ fn csv_to_vec(csv: &str) -> Vec<String> {
|
|||
|
||||
/// TODO Document
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Blog {
|
||||
pub struct Site {
|
||||
pub name: String,
|
||||
pub author: String,
|
||||
pub topics: Vec<String>,
|
||||
}
|
||||
|
||||
impl Blog {
|
||||
pub fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Blog, Box<dyn std::error::Error>> {
|
||||
let name = get_input("Please enter a name for the blog: ", reader)?;
|
||||
let author = get_input("Please enter the blog author's name: ", reader)?;
|
||||
let topics = get_input("Please enter comma-separated blog topics: ", reader)?;
|
||||
impl Site {
|
||||
pub fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Site, Box<dyn std::error::Error>> {
|
||||
let name = get_input("Please enter a name for the site: ", reader)?;
|
||||
let author = get_input("Please enter the site author's name: ", reader)?;
|
||||
let topics = get_input("Please enter comma-separated site topics: ", reader)?;
|
||||
let topics = csv_to_vec(&topics);
|
||||
let blog = Blog { name, author, topics };
|
||||
let site = Site { name, author, topics };
|
||||
|
||||
Ok(blog)
|
||||
trace!("Site: {:?}", site);
|
||||
Ok(site)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,9 +146,10 @@ pub struct Credentials {
|
|||
|
||||
impl Credentials {
|
||||
pub fn new_from_input<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<Credentials, Box<dyn std::error::Error>> {
|
||||
let user = get_input("Please enter an username for the blog admin: ", reader)?;
|
||||
let user = get_input("Please enter an username for the site admin: ", reader)?;
|
||||
const PASSWORD_LEN: usize = 32;
|
||||
let password = auth::generate_secret(PASSWORD_LEN)?;
|
||||
info!("Site admin password generated");
|
||||
let password_file = dir.as_ref().join("admin.pass");
|
||||
common::str_to_ro_file(&password, password_file)?;
|
||||
let password = auth::generate_argon2_phc(&password)?;
|
||||
|
@ -128,6 +158,7 @@ impl Credentials {
|
|||
let token = auth::generate_secret(TOKEN_LEN)?;
|
||||
let token = token.as_bytes();
|
||||
let token = auth::BASE32_NOPAD.encode(token);
|
||||
info!("Site admin TOTP token generated");
|
||||
let token_file = dir.as_ref().join("admin.totp");
|
||||
common::str_to_ro_file(&token, token_file)?;
|
||||
|
||||
|
@ -146,49 +177,45 @@ pub struct DocPaths {
|
|||
|
||||
impl DocPaths {
|
||||
pub fn new<P: AsRef<Path>>(dir: P) -> Result<DocPaths, Box<dyn std::error::Error>> {
|
||||
debug!("Creating site DocPaths");
|
||||
let dir = dir.as_ref().display();
|
||||
let templates = format!("{}/blog/templates", dir);
|
||||
let webroot = format!("{}/blog/webroot", dir);
|
||||
let templates = format!("{}/site/templates", dir);
|
||||
let webroot = format!("{}/site/webroot", dir);
|
||||
let docpaths = DocPaths { templates, webroot };
|
||||
|
||||
trace!("Site DocPaths: {:?}", docpaths);
|
||||
Ok(docpaths)
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO Document
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct LogConfig {
|
||||
level: String
|
||||
}
|
||||
|
||||
/// TODO Document
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct AppConfig {
|
||||
pub blog: Blog,
|
||||
pub site: Site,
|
||||
pub creds: Credentials,
|
||||
pub logging: LogConfig,
|
||||
pub docpaths: DocPaths,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn new<T>(config: T) -> Result<AppConfig, Box<dyn std::error::Error>>
|
||||
where T: AsRef<Path> {
|
||||
pub fn from_path<T: AsRef<Path>>(config: T) -> Result<AppConfig, Box<dyn std::error::Error>> {
|
||||
debug!("Loading site configuration from {}", &config.as_ref().display());
|
||||
let config = std::fs::read_to_string(config)?;
|
||||
|
||||
trace!("Parsing configuration TOML");
|
||||
let app_config: AppConfig = toml::from_str(&config)?;
|
||||
|
||||
Ok(app_config)
|
||||
}
|
||||
|
||||
pub fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig, Box<dyn std::error::Error>> {
|
||||
debug!("Generating new site configuration");
|
||||
let docpaths = DocPaths::new(&dir)?;
|
||||
let blog = Blog::new_from_input(reader)?;
|
||||
let site = Site::new_from_input(reader)?;
|
||||
let creds = Credentials::new_from_input(&dir, reader)?;
|
||||
let level = format!("INFO");
|
||||
let logging = LogConfig { level };
|
||||
|
||||
let config = AppConfig {
|
||||
blog,
|
||||
site,
|
||||
creds,
|
||||
logging,
|
||||
docpaths,
|
||||
};
|
||||
|
||||
|
@ -199,12 +226,13 @@ impl AppConfig {
|
|||
}
|
||||
|
||||
fn create_paths(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
debug!("Creating site filesystem tree");
|
||||
create_dir_all(&self.docpaths.templates)?;
|
||||
create_dir_all(format!("{}/static/ext", &self.docpaths.webroot))?;
|
||||
create_dir_all(format!("{}/main/ext", &self.docpaths.webroot))?;
|
||||
create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;
|
||||
|
||||
for topic in &self.blog.topics {
|
||||
for topic in &self.site.topics {
|
||||
let topic = common::slugify(&topic);
|
||||
|
||||
create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
|
||||
|
@ -214,6 +242,7 @@ impl AppConfig {
|
|||
}
|
||||
|
||||
fn write<P: AsRef<Path>>(&self, dir: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
debug!("Writing site configuration to disk");
|
||||
let config = toml::to_string_pretty(&self)?;
|
||||
let conf_path = &dir.as_ref().join("config.toml");
|
||||
common::str_to_ro_file(&config, &conf_path)?;
|
||||
|
@ -224,11 +253,10 @@ impl AppConfig {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile;
|
||||
|
||||
#[test]
|
||||
fn build_run_config() {
|
||||
let arg_vec = vec!["caty-blog", "run", "./test_files/test-config.toml"];
|
||||
let arg_vec = vec!["arse", "run", "./test_files/test-config.toml"];
|
||||
let matches = args().get_matches_from(arg_vec);
|
||||
let config = runner_config(matches);
|
||||
assert!(config.is_ok());
|
||||
|
@ -238,7 +266,7 @@ mod tests {
|
|||
fn build_config_from_input() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// Setup all target fields
|
||||
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
|
||||
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
|
||||
let config = AppConfig::generate(&dir, &mut src);
|
||||
assert!(config.is_ok());
|
||||
|
||||
|
@ -246,21 +274,21 @@ mod tests {
|
|||
let config_path = &tmp_dir.join("config.toml");
|
||||
let admin = &tmp_dir.join("admin.pass");
|
||||
let token = &tmp_dir.join("admin.totp");
|
||||
let blog = &tmp_dir.join("blog");
|
||||
let templates = &tmp_dir.join("blog/templates");
|
||||
let webroot = &tmp_dir.join("blog/webroot");
|
||||
let static_ext = &tmp_dir.join("blog/webroot/static/ext");
|
||||
let main_ext = &tmp_dir.join("blog/webroot/main/ext");
|
||||
let main_posts = &tmp_dir.join("blog/webroot/main/posts");
|
||||
let one_ext = &tmp_dir.join("blog/webroot/one/ext");
|
||||
let one_posts = &tmp_dir.join("blog/webroot/one/posts");
|
||||
let two_ext = &tmp_dir.join("blog/webroot/two/ext");
|
||||
let two_posts = &tmp_dir.join("blog/webroot/two/posts");
|
||||
let three_ext = &tmp_dir.join("blog/webroot/three/ext");
|
||||
let three_posts = &tmp_dir.join("blog/webroot/three/posts");
|
||||
let and_more_ext = &tmp_dir.join("blog/webroot/and-more/ext");
|
||||
let and_more_posts = &tmp_dir.join("blog/webroot/and-more/posts");
|
||||
let core = vec![config_path, admin, token, blog, templates,
|
||||
let site = &tmp_dir.join("site");
|
||||
let templates = &tmp_dir.join("site/templates");
|
||||
let webroot = &tmp_dir.join("site/webroot");
|
||||
let static_ext = &tmp_dir.join("site/webroot/static/ext");
|
||||
let main_ext = &tmp_dir.join("site/webroot/main/ext");
|
||||
let main_posts = &tmp_dir.join("site/webroot/main/posts");
|
||||
let one_ext = &tmp_dir.join("site/webroot/one/ext");
|
||||
let one_posts = &tmp_dir.join("site/webroot/one/posts");
|
||||
let two_ext = &tmp_dir.join("site/webroot/two/ext");
|
||||
let two_posts = &tmp_dir.join("site/webroot/two/posts");
|
||||
let three_ext = &tmp_dir.join("site/webroot/three/ext");
|
||||
let three_posts = &tmp_dir.join("site/webroot/three/posts");
|
||||
let and_more_ext = &tmp_dir.join("site/webroot/and-more/ext");
|
||||
let and_more_posts = &tmp_dir.join("site/webroot/and-more/posts");
|
||||
let core = vec![config_path, admin, token, site, templates,
|
||||
webroot, static_ext, main_ext, main_posts,
|
||||
one_ext, one_posts, two_ext, two_posts,
|
||||
three_ext, three_posts, and_more_ext, and_more_posts];
|
||||
|
@ -271,8 +299,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn handle_csv_topics() {
|
||||
let reference_topics: Vec<String> = vec![format!("One"), format!("Two"), format!("Three"), format!("And More")];
|
||||
let topics = format!("One, Two, Three, And More");
|
||||
let reference_topics: Vec<String> = vec!["One".to_owned(),
|
||||
"Two".to_owned(),
|
||||
"Three".to_owned(),
|
||||
"And More".to_owned()];
|
||||
let topics = "One, Two, Three, And More".to_owned();
|
||||
assert_eq!(reference_topics, csv_to_vec(&topics))
|
||||
}
|
||||
|
||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -1,10 +1,12 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
// Placeholder for logging setup
|
|
@ -1,10 +1,12 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
@ -12,11 +14,13 @@ use std::sync::Arc;
|
|||
use super::config::AppConfig;
|
||||
use super::common;
|
||||
|
||||
use log::{debug, trace};
|
||||
use pulldown_cmark::{Parser, html};
|
||||
use tera::{Tera, Context};
|
||||
|
||||
mod default;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Engine {
|
||||
pub app: Arc<AppConfig>,
|
||||
pub topic_slug: String,
|
||||
|
@ -26,8 +30,9 @@ pub struct Engine {
|
|||
|
||||
impl Engine {
|
||||
pub fn new(a: Arc<AppConfig>, ts: &str, tmpl: &str, inst: Tera) -> Engine {
|
||||
trace!("Loading rendering engine");
|
||||
Engine {
|
||||
app: a.clone(),
|
||||
app: a,
|
||||
topic_slug: ts.to_owned(),
|
||||
template: tmpl.to_owned(),
|
||||
instance: inst
|
||||
|
@ -35,24 +40,28 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_default_template() -> Result<Tera, Box<dyn std::error::Error>> {
|
||||
pub(crate) fn load_default_template() -> Result<Tera, Box<dyn std::error::Error>> {
|
||||
trace!("Loading default rendering template");
|
||||
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;
|
||||
pub(crate) fn render_topic(engine: Engine) -> Result<String, Box<dyn std::error::Error>> {
|
||||
debug!("Rendering topic: '{}'", &engine.topic_slug);
|
||||
let site = &engine.app.site;
|
||||
let topic_data = load_topic(&engine)?;
|
||||
let mut context = Context::new();
|
||||
context.insert("blog", blog);
|
||||
context.insert("site", site);
|
||||
context.insert("posts", &topic_data);
|
||||
let output = engine.instance.render(&engine.template, &context)?;
|
||||
|
||||
trace!("Rendered content for topic: {}\n{}", &engine.topic_slug, output);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn load_topic(engine: &Engine) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
trace!("Loading topic content for '{}'", &engine.topic_slug);
|
||||
let topic_path = Path::new(&engine.app.docpaths.webroot).join(&engine.topic_slug).join("posts");
|
||||
let pat = format!("{}/*.md", topic_path.display());
|
||||
let paths = common::path_matches(&pat)?;
|
||||
|
@ -60,8 +69,10 @@ fn load_topic(engine: &Engine) -> Result<Vec<String>, Box<dyn std::error::Error>
|
|||
}
|
||||
|
||||
fn read_to_html(paths: Vec<PathBuf>) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
debug!("Rendering Markdown to HTML");
|
||||
let mut contents: Vec<String> = Vec::new();
|
||||
for path in paths {
|
||||
trace!("Rendering {} to HTML", &path.display());
|
||||
let buf = std::fs::read_to_string(path)?;
|
||||
let parser = Parser::new(&buf);
|
||||
let mut html_output = String::new();
|
||||
|
@ -77,7 +88,6 @@ mod tests {
|
|||
use super::*;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use tempfile;
|
||||
|
||||
#[test]
|
||||
fn check_default_template() {
|
||||
|
@ -88,7 +98,7 @@ mod tests {
|
|||
#[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 mut src: &[u8] = b"Site 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);
|
||||
|
@ -105,10 +115,10 @@ Very cool, but maybe not super useful
|
|||
Super Wow!
|
||||
"#;
|
||||
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/one/posts/post1.md")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/one/posts/post1.md")).unwrap();
|
||||
f.write_all(&post.as_bytes()).unwrap();
|
||||
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/one/posts/post2.md")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/one/posts/post2.md")).unwrap();
|
||||
f.write_all(&post2.as_bytes()).unwrap();
|
||||
|
||||
let page = render_topic(engine).unwrap();
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
pub const TEMPLATE: &str = r#"
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
pub(crate) 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>
|
||||
<title>{{ site.name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<center>
|
||||
<h1>{{ blog.name }}</h1>
|
||||
<h1>{{ site.name }}</h1>
|
||||
<nav>
|
||||
<a href="./">Home</a>
|
||||
{%- for topic in blog.topics %}
|
||||
{%- for topic in site.topics %}
|
||||
<a href="./{{ topic | slugify }}">{{ topic }}</a>
|
||||
{%- endfor -%}
|
||||
</nav>
|
||||
|
@ -37,7 +39,7 @@ pub const TEMPLATE: &str = r#"
|
|||
{% endif %}
|
||||
</main>
|
||||
<footer>
|
||||
<p>© {{ blog.author }}</p>
|
||||
<p>© {{ site.author }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
// A Rust Site Engine
|
||||
// Copyright 2020-2021 Anthony Martinez
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
/*
|
||||
A Rust Site Engine
|
||||
Copyright 2020-2021 Anthony Martinez
|
||||
|
||||
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
*/
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::fs::File;
|
||||
|
@ -12,7 +14,8 @@ use std::io::prelude::*;
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{Body, Request, Response};//, StatusCode};
|
||||
use hyper::{Body, Request, Response};
|
||||
use log::{debug, info};
|
||||
use routerify::{prelude::*, Router};
|
||||
|
||||
use super::config::AppConfig;
|
||||
|
@ -20,8 +23,9 @@ use super::render;
|
|||
|
||||
|
||||
pub fn router(app: Arc<AppConfig>) -> Router<Body, Infallible> {
|
||||
debug!("Building site router");
|
||||
Router::builder()
|
||||
.data(app.clone())
|
||||
.data(app)
|
||||
.get("/", index_handler)
|
||||
.get("/:topic", topic_handler)
|
||||
.get("/:topic/ext/:fname", topic_assets)
|
||||
|
@ -32,6 +36,7 @@ pub fn router(app: Arc<AppConfig>) -> Router<Body, Infallible> {
|
|||
|
||||
/// Handler for "/"
|
||||
async fn index_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
info!("Handling request to '/'");
|
||||
let app = req.data::<Arc<AppConfig>>().unwrap();
|
||||
topic_posts(app.clone(), "main".to_owned()).await
|
||||
}
|
||||
|
@ -40,13 +45,14 @@ async fn index_handler(req: Request<Body>) -> Result<Response<Body>, Infallible>
|
|||
async fn topic_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
let app = req.data::<Arc<AppConfig>>().unwrap();
|
||||
let topic = req.param("topic").unwrap();
|
||||
info!("Handling request to '/{}'", &topic);
|
||||
topic_posts(app.clone(), topic.to_owned()).await
|
||||
}
|
||||
|
||||
/// Called by topic_handler to dynamically generate topic pages
|
||||
async fn topic_posts(app: Arc<AppConfig>, topic: String) -> Result<Response<Body>, Infallible> {
|
||||
let instance = render::load_default_template().unwrap();
|
||||
let engine = render::Engine::new(app.clone(), &topic, "default.tmpl", instance);
|
||||
let engine = render::Engine::new(app, &topic, "default.tmpl", instance);
|
||||
let output = render::render_topic(engine).unwrap();
|
||||
Ok(Response::new(Body::from(output)))
|
||||
}
|
||||
|
@ -55,6 +61,7 @@ async fn topic_posts(app: Arc<AppConfig>, topic: String) -> Result<Response<Body
|
|||
async fn static_assets(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
let app = req.data::<Arc<AppConfig>>().unwrap();
|
||||
let resource = req.param("fname").unwrap();
|
||||
info!("Handling static asset: '/static/{}'", &resource);
|
||||
let static_path = Path::new(&app.docpaths.webroot).join("static").join(resource);
|
||||
let mut f = File::open(static_path).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
|
@ -67,6 +74,7 @@ async fn topic_assets(req: Request<Body>) -> Result<Response<Body>, Infallible>
|
|||
let app = req.data::<Arc<AppConfig>>().unwrap();
|
||||
let topic = req.param("topic").unwrap();
|
||||
let resource = req.param("fname").unwrap();
|
||||
info!("Handling static asset: '/{}/ext/{}'", &topic, &resource);
|
||||
let topic_asset_path = Path::new(&app.docpaths.webroot).join(topic).join("ext").join(resource);
|
||||
let mut f = File::open(topic_asset_path).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
|
@ -85,14 +93,13 @@ mod tests {
|
|||
use crate::config::AppConfig;
|
||||
use hyper::{Body, Request, StatusCode, Server, Client};
|
||||
use routerify::RouterService;
|
||||
use tempfile;
|
||||
use tokio::sync::oneshot::channel;
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_all_handlers() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
|
||||
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
|
||||
let app = AppConfig::generate(&dir, &mut src).unwrap();
|
||||
let app = Arc::new(app);
|
||||
|
||||
|
@ -101,7 +108,7 @@ mod tests {
|
|||
|
||||
Main Important Test
|
||||
"#;
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/main/posts/index.md")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/main/posts/index.md")).unwrap();
|
||||
f.write_all(&index_page.as_bytes()).unwrap();
|
||||
|
||||
let topic_page = r#"
|
||||
|
@ -109,17 +116,17 @@ Main Important Test
|
|||
|
||||
One Important Test
|
||||
"#;
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/one/posts/index.md")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/one/posts/index.md")).unwrap();
|
||||
f.write_all(&topic_page.as_bytes()).unwrap();
|
||||
|
||||
let topic_asset = b"One Static File\n";
|
||||
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/one/ext/one-static")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/one/ext/one-static")).unwrap();
|
||||
f.write_all(topic_asset).unwrap();
|
||||
|
||||
let static_asset = b"Static File\n";
|
||||
|
||||
let mut f = File::create(&dir.path().join("blog/webroot/static/main-static")).unwrap();
|
||||
let mut f = File::create(&dir.path().join("site/webroot/static/main-static")).unwrap();
|
||||
f.write_all(static_asset).unwrap();
|
||||
|
||||
let router = router(app.clone());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[blog]
|
||||
[site]
|
||||
name = "My Awesome Blog!"
|
||||
author = "Neo"
|
||||
topics = ["one", "two", "three"]
|
||||
|
@ -8,9 +8,6 @@ user = "admin"
|
|||
password = "123456"
|
||||
token = "my very secret token"
|
||||
|
||||
[logging]
|
||||
level = "INFO"
|
||||
|
||||
[docpaths]
|
||||
templates = "/var/www/blog/templates"
|
||||
webroot = "/var/www/blog/webroot"
|
||||
templates = "/var/www/site/templates"
|
||||
webroot = "/var/www/site/webroot"
|
||||
|
|
Loading…
Reference in New Issue