Add support for config file in image directory
This config file will be loaded together with the images and defines the passwords for login and some other parameters.
This commit is contained in:
94
src/main.rs
94
src/main.rs
@ -1,8 +1,8 @@
|
||||
use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::{OsStr, OsString}, fs::File, io::{BufReader, Seek, Cursor, BufRead}, env::args_os, os::unix::prelude::OsStrExt, cmp::Reverse, sync::{Arc, RwLock}, collections::HashMap};
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
|
||||
use axum_sessions::{async_session::MemoryStore, SessionLayer, extractors::{ReadableSession, WritableSession}};
|
||||
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect, Response}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
|
||||
use axum_sessions::{async_session::{MemoryStore, SessionStore}, SessionLayer, extractors::{ReadableSession, WritableSession}};
|
||||
use axum_template::{RenderHtml, engine::Engine};
|
||||
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
|
||||
use chrono::{DateTime, Utc};
|
||||
@ -15,6 +15,7 @@ use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
#[global_allocator]
|
||||
@ -22,7 +23,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
// TODO
|
||||
// * Polish the css
|
||||
// * Add configuration file
|
||||
// * Brute-force protection / rate-limiting
|
||||
// * Support for headlines
|
||||
// * Image cache cleanup
|
||||
|
||||
@ -31,9 +32,11 @@ type ImageDir = PathBuf;
|
||||
type SecretKey = [u8; 64];
|
||||
type ImageListCache = Arc<RwLock<Vec<ImageInfo>>>;
|
||||
type ImageCache = Arc<RwLock<HashMap<OsString, (DateTime<Utc>, Vec<u8>)>>>;
|
||||
type Config = Arc<RwLock<config::Config>>;
|
||||
|
||||
#[derive(Clone, extract::FromRef)]
|
||||
struct ApplicationState {
|
||||
config: Config,
|
||||
engine: TemplateEngine,
|
||||
image_cache: ImageCache,
|
||||
image_dir: ImageDir,
|
||||
@ -60,15 +63,19 @@ async fn main() {
|
||||
|
||||
let sessions = MemoryStore::new();
|
||||
let image_dir = PathBuf::from(image_path);
|
||||
let config = Config::default();
|
||||
let image_cache = ImageCache::default();
|
||||
let image_list_cache = ImageListCache::default();
|
||||
|
||||
tokio::spawn(update_image_list_cache_job(
|
||||
tokio::spawn(update_config_and_image_list_cache_job(
|
||||
image_dir.clone(),
|
||||
image_list_cache.clone()
|
||||
config.clone(),
|
||||
image_list_cache.clone(),
|
||||
sessions.clone(),
|
||||
));
|
||||
|
||||
let mut jinja = minijinja::Environment::new();
|
||||
jinja.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
|
||||
jinja.add_template("base", include_str!("../templates/base.html")).unwrap();
|
||||
jinja.add_template("index", include_str!("../templates/index.html")).unwrap();
|
||||
jinja.add_template("login", include_str!("../templates/login.html")).unwrap();
|
||||
@ -79,6 +86,7 @@ async fn main() {
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index.layer(CompressionLayer::new())))
|
||||
.route("/config.yaml", get(example_config))
|
||||
.route("/authenticate", post(authenticate))
|
||||
.route("/image/:encrypted_filename_hex", get(converted_image))
|
||||
.layer(TraceLayer::new_for_http()
|
||||
@ -87,6 +95,7 @@ async fn main() {
|
||||
)
|
||||
.layer(session_layer)
|
||||
.with_state(ApplicationState {
|
||||
config,
|
||||
engine: Engine::from(jinja),
|
||||
image_cache,
|
||||
image_dir,
|
||||
@ -102,13 +111,50 @@ async fn main() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_image_list_cache_job(image_dir: ImageDir, image_cache: ImageListCache) {
|
||||
async fn update_config_and_image_list_cache_job(
|
||||
image_dir: ImageDir,
|
||||
config: Config,
|
||||
image_cache: ImageListCache,
|
||||
sessions: MemoryStore,
|
||||
) {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
// Read config
|
||||
let config_file = image_dir.join("config.yaml");
|
||||
let config = config.clone();
|
||||
let invalidate_sessions = if config_file.exists() {
|
||||
task::spawn_blocking(move || {
|
||||
match config::read_from_file(&config_file) {
|
||||
Ok(new_config) => {
|
||||
let mut config = config.write().unwrap();
|
||||
if new_config != *config {
|
||||
tracing::debug!("New config: {:?}", new_config);
|
||||
*config = new_config;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tracing::error!("Could not read config, using default: {:#}", error);
|
||||
*config.write().unwrap() = config::Config::default();
|
||||
true
|
||||
}
|
||||
}
|
||||
}).await.unwrap()
|
||||
} else {
|
||||
*config.write().unwrap() = config::Config::default();
|
||||
true
|
||||
};
|
||||
if invalidate_sessions {
|
||||
sessions.clear_store().await.ok();
|
||||
}
|
||||
|
||||
// Update image list
|
||||
let image_dir = image_dir.clone();
|
||||
let images = task::spawn_blocking(move || {
|
||||
// TODO: Only update images with changed modification times
|
||||
@ -140,19 +186,28 @@ pub struct ImageInfo {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IndexTempalte {
|
||||
pub struct LoginTemplate {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IndexTemplate {
|
||||
title: String,
|
||||
top_message: Option<String>,
|
||||
images: Vec<ImageInfo>,
|
||||
}
|
||||
|
||||
async fn index(
|
||||
engine: TemplateEngine,
|
||||
State(config): State<Config>,
|
||||
State(image_list_cache): State<ImageListCache>,
|
||||
State(secret_key): State<SecretKey>,
|
||||
session: ReadableSession,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
) -> Result<Response, AppError> {
|
||||
let logged_in = session.get::<()>("logged_in").is_some();
|
||||
|
||||
let config = config.read().unwrap();
|
||||
|
||||
if logged_in {
|
||||
let images = image_list_cache.read().unwrap();
|
||||
|
||||
@ -171,18 +226,22 @@ async fn index(
|
||||
|
||||
images.sort_by_key(|entry| Reverse(entry.created));
|
||||
|
||||
Ok(RenderHtml("index", engine, IndexTempalte {
|
||||
title: "Some pictures".into(),
|
||||
Ok(RenderHtml("index", engine, IndexTemplate {
|
||||
title: config.title.clone(),
|
||||
top_message: config.top_message.clone(),
|
||||
images,
|
||||
}))
|
||||
}).into_response())
|
||||
} else {
|
||||
Ok(RenderHtml("login", engine, IndexTempalte {
|
||||
title: "Some pictures".into(),
|
||||
images: vec![],
|
||||
}))
|
||||
Ok(RenderHtml("login", engine, LoginTemplate {
|
||||
title: config.title.clone(),
|
||||
}).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
async fn example_config() -> &'static str {
|
||||
include_str!("../config.yaml.example")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthenticateForm {
|
||||
password: String,
|
||||
@ -190,9 +249,10 @@ struct AuthenticateForm {
|
||||
|
||||
async fn authenticate(
|
||||
mut session: WritableSession,
|
||||
Form(form): Form<AuthenticateForm>
|
||||
State(config): State<Config>,
|
||||
Form(form): Form<AuthenticateForm>,
|
||||
) -> Redirect {
|
||||
if form.password == "testle" {
|
||||
if config.read().unwrap().passwords.contains(&form.password) {
|
||||
session.insert("logged_in", ()).ok();
|
||||
}
|
||||
Redirect::to("/")
|
||||
|
Reference in New Issue
Block a user