Add simple in-memory cache for encoded images

This commit is contained in:
Klemens Schölhorn 2023-03-20 00:15:20 +01:00
parent b2207e4f48
commit be24603fff

View File

@ -1,4 +1,4 @@
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}; 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, collections::HashMap};
use anyhow::{Context, Result, anyhow}; 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::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
@ -10,7 +10,7 @@ use error::AppError;
use exif::Exif; use exif::Exif;
use image::{imageops::FilterType, DynamicImage}; use image::{imageops::FilterType, DynamicImage};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tokio::task; use tokio::{task, sync::RwLock};
use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer}; use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer};
use tracing::Level; use tracing::Level;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
@ -29,10 +29,12 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
type TemplateEngine = Engine<minijinja::Environment<'static>>; type TemplateEngine = Engine<minijinja::Environment<'static>>;
type ImageDir = PathBuf; type ImageDir = PathBuf;
type SecretKey = [u8; 64]; type SecretKey = [u8; 64];
type ImageCache = Arc<RwLock<HashMap<OsString, (DateTime<Utc>, Vec<u8>)>>>;
#[derive(Clone, extract::FromRef)] #[derive(Clone, extract::FromRef)]
struct ApplicationState { struct ApplicationState {
engine: TemplateEngine, engine: TemplateEngine,
image_cache: ImageCache,
image_dir: ImageDir, image_dir: ImageDir,
secret_key: SecretKey, secret_key: SecretKey,
} }
@ -74,6 +76,7 @@ async fn main() {
.layer(session_layer) .layer(session_layer)
.with_state(ApplicationState { .with_state(ApplicationState {
engine: Engine::from(jinja), engine: Engine::from(jinja),
image_cache: Default::default(),
image_dir: PathBuf::from(image_path), image_dir: PathBuf::from(image_path),
secret_key, secret_key,
}); });
@ -158,6 +161,7 @@ async fn authenticate(
async fn converted_image( async fn converted_image(
extract::Path(encrypted_filename_hex): extract::Path<String>, extract::Path(encrypted_filename_hex): extract::Path<String>,
State(image_cache): State<ImageCache>,
State(image_dir): State<ImageDir>, State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>, State(secret_key): State<SecretKey>,
session: ReadableSession, session: ReadableSession,
@ -175,19 +179,51 @@ async fn converted_image(
let chacha = XChaCha20Poly1305::new(chacha_key); let chacha = XChaCha20Poly1305::new(chacha_key);
chacha.decrypt(nonce, ciphertext) chacha.decrypt(nonce, ciphertext)
}.with_context(|| format!("Could not decrypt filename {}", encrypted_filename_hex))?; }.with_context(|| format!("Could not decrypt filename {}", encrypted_filename_hex))?;
let image_name = OsStr::from_bytes(&image_name);
let image_path = image_dir.join(OsStr::from_bytes(&image_name)); let image_path = image_dir.join(image_name);
image_path.exists() image_path.exists()
.then_some(()) .then_some(())
.ok_or(anyhow!("Requested image not found!")) .ok_or(anyhow!("Requested image not found!"))
.context(StatusCode::NOT_FOUND)?; .context(StatusCode::NOT_FOUND)?;
// Check if we have the file already in cache
let image_modified: Option<DateTime<Utc>> = std::fs::metadata(&image_path)
.and_then(|m| m.modified())
.map(DateTime::from)
.ok();
let cached_buffer = if let Some(image_modified) = image_modified {
image_cache
.read().await
.get(image_name)
.filter(|(cache_modified, _)| *cache_modified == image_modified)
.map(|(_, image_buffer)| image_buffer)
.cloned()
} else { None };
let image_buffer = match cached_buffer {
Some(image_buffer) => {
tracing::debug!("Read {:?} from cache", image_name);
image_buffer
}
None => {
let image_buffer = task::spawn_blocking(move || { let image_buffer = task::spawn_blocking(move || {
convert_image(&image_path) convert_image(&image_path)
.with_context(|| format!("Could not convert image {:?}", image_path)) .with_context(|| format!("Could not convert image {:?}", image_path))
}).await??; }).await??;
if let Some(image_modified) = image_modified {
tracing::debug!("Add {:?} ({}k) to cache", image_name, image_buffer.len() / 1024);
image_cache
.write().await
.insert(image_name.to_owned(), (image_modified, image_buffer.clone()));
}
image_buffer
}
};
Ok(( Ok((
[ [
(header::CONTENT_TYPE, "image/webp"), (header::CONTENT_TYPE, "image/webp"),