Compare commits

...

10 Commits

Author SHA1 Message Date
Klemens Schölhorn 9c319d6949 Change tower logging level to error 2024-01-21 17:56:01 +01:00
Klemens Schölhorn 54295ea098 Change port to 9030 2024-01-21 17:55:36 +01:00
Klemens Schölhorn 48a3a206a3 Reduce resource usage of metadata update task
This task previously read all images file's EXIF data every 60s, which
leads to a very large CPU usage for seaf_fuse.

The obvious fix of checking for modifications times and only read the
change files does not work with seaf_fuse, because stat is not cheaper
than actually opening and reading the file.

So instead, we now only read the file list and the modification time of
the image directory itself and update the image metadata only if there
are changes. We also update the metadata at least once every hour to
detect modified files on normal filesystems.

Interestingly, seaf_fuse will update the mtime of a directory if any file
in that directory is changed, so the logic implemented in this commit
will actually detect image changes with seaf_fuse instantly.
2024-01-21 17:42:22 +01:00
Klemens Schölhorn 68c7fd2ac4 Limit number of parallel image conversions
Tokio will happy start hundreds of parallel image conversions, which
overwhelms seaf_fuse, so limit the parallel conversion to 4 using a
semaphore.
2024-01-21 17:39:55 +01:00
Klemens Schölhorn 17d0bae1aa Fix parsing of exif datetimes
We previously tried to parse the exif datetime as an ISO datetime
string, however the date parse uses colons instead of dashes in exif.
This meant that the modification time was used for every image instead
of the exif date.
2023-04-21 23:08:36 +02:00
Klemens Schölhorn 03ab64ddbc Fix spelling mistake in example config 2023-04-07 00:43:16 +02:00
Klemens Schölhorn 8f004e5e65 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.
2023-04-07 00:37:54 +02:00
Klemens Schölhorn 68f50b5292 Use MemoryStore for sessions to be able to clear sessions 2023-04-07 00:23:15 +02:00
Klemens Schölhorn 9af4831efa Add cache for image info list
Preparing the images list involves reading a directory (which might be
slow on non-local directories) and opening every file to read the exif
data. To avoid having to do this on every request, we do it every 60s in
a background job and only read from the cache on requests.
2023-04-06 22:11:12 +02:00
Klemens Schölhorn 6206d0ea58 Use std RwLock instead of the tokio one
tokio recommends to use the std synchronization primitives if the locked
value is just data instead of a shared resource.
2023-04-06 22:06:29 +02:00
6 changed files with 369 additions and 90 deletions

161
Cargo.lock generated
View File

@ -27,6 +27,12 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -378,18 +384,17 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.24"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time 0.1.45",
"wasm-bindgen",
"winapi",
"windows-targets 0.48.5",
]
[[package]]
@ -437,7 +442,7 @@ dependencies = [
"rand",
"sha2 0.10.6",
"subtle",
"time 0.3.20",
"time",
"version_check",
]
@ -679,9 +684,15 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "headers"
version = "0.3.8"
@ -868,12 +879,23 @@ dependencies = [
"kamadak-exif",
"minijinja",
"serde",
"serde_yaml",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
@ -1045,7 +1067,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.45.0",
]
@ -1358,6 +1380,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.5"
@ -1498,17 +1533,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.20"
@ -1741,6 +1765,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
[[package]]
name = "valuable"
version = "0.1.0"
@ -1763,12 +1793,6 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1875,13 +1899,13 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
]
[[package]]
@ -1890,7 +1914,7 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
"windows-targets 0.42.1",
]
[[package]]
@ -1899,13 +1923,28 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@ -1914,42 +1953,84 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "zeroize"
version = "1.5.7"

View File

@ -16,6 +16,7 @@ jemallocator = "0.5.0"
kamadak-exif = "0.5.5"
minijinja = "0.30.4"
serde = { version = "1.0.152", features = ["derive"] }
serde_yaml = "0.9.21"
tokio = { version = "1.25.0", features = ["full"] }
tower-http = { version = "0.3.5", features = ["fs", "trace", "compression-br"] }
tracing = "0.1.37"

14
config.yaml.example Normal file
View File

@ -0,0 +1,14 @@
# Save this file as config.yaml into the image directory
# Config and image list will be updated every 60s. If there is
# an error with the config (or no config at all), no access to
# the images will be granted; check the log in case of errors.
# Changing the config invalidates all active sessions.
title: Website title
passwords:
- password1
- password2
# Optional, will be displayed above images
top-message: "Please do not share or save these images"

18
src/config.rs Normal file
View File

@ -0,0 +1,18 @@
use std::{path::Path, fs::File};
use anyhow::Result;
use serde::Deserialize;
#[derive(Clone, Default, Debug, PartialEq, Deserialize)]
pub struct Config {
pub title: String,
#[serde(alias = "top-message")]
pub top_message: Option<String>,
pub passwords: Vec<String>,
}
pub fn read_from_file(config_file: &Path) -> Result<Config> {
let file = File::open(config_file)?;
Ok(serde_yaml::from_reader(file)?)
}

View File

@ -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, collections::HashMap};
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, HashSet}, time::{Instant, Duration}};
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::CookieStore, 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};
@ -10,11 +10,12 @@ use error::AppError;
use exif::Exif;
use image::{imageops::FilterType, DynamicImage};
use serde::{Serialize, Deserialize};
use tokio::{task, sync::RwLock};
use tokio::{task, sync::Semaphore};
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,21 +23,28 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
// TODO
// * Polish the css
// * Add configuration file
// * Brute-force protection / rate-limiting
// * Support for headlines
// * Cache generated images (+ cache cleanup)
// * Image cache cleanup
// * Limit session time (inactivity timeout)
type TemplateEngine = Engine<minijinja::Environment<'static>>;
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>>;
type CpuTaskLimit = Arc<Semaphore>;
#[derive(Clone, extract::FromRef)]
struct ApplicationState {
config: Config,
engine: TemplateEngine,
image_cache: ImageCache,
image_dir: ImageDir,
image_list_cache: ImageListCache,
secret_key: SecretKey,
cpu_task_limit: CpuTaskLimit,
}
#[tokio::main]
@ -46,7 +54,7 @@ async fn main() {
let mut secret_key: SecretKey = [0; 64];
OsRng.fill_bytes(&mut secret_key);
let default_tracing = "image_gallery=debug,tower_http=info".into();
let default_tracing = "image_gallery=debug,tower_http=error".into();
let tracing_filter = EnvFilter::try_from_default_env().unwrap_or(default_tracing);
let tracing_formatter = tracing_subscriber::fmt::layer()
.with_target(false)
@ -56,17 +64,32 @@ async fn main() {
.with(tracing_formatter)
.init();
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_config_and_image_list_cache_job(
image_dir.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();
let session_layer = SessionLayer::new(CookieStore::new(), &secret_key)
let session_layer = SessionLayer::new(sessions, &secret_key)
.with_cookie_name("session")
.with_session_ttl(None);
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()
@ -75,13 +98,16 @@ async fn main() {
)
.layer(session_layer)
.with_state(ApplicationState {
config,
engine: Engine::from(jinja),
image_cache: Default::default(),
image_dir: PathBuf::from(image_path),
image_cache,
image_dir,
image_list_cache,
secret_key,
cpu_task_limit: Arc::new(Semaphore::new(4)),
});
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
let addr = SocketAddr::from(([0, 0, 0, 0], 9030));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
@ -89,7 +115,121 @@ async fn main() {
.unwrap();
}
#[derive(Debug, Serialize, Default)]
async fn update_config_and_image_list_cache_job(
image_dir: ImageDir,
config: Config,
image_metadata_cache: ImageListCache,
sessions: MemoryStore,
) {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut last_images = HashSet::new();
let mut last_image_dir_change = DateTime::<Utc>::UNIX_EPOCH;
let mut last_forced_update = Instant::now();
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();
}
// Read the modification time of the image dir
// Note that some fuse implementations (e.g. seaf_fuse) will update
// the mtime of the directory when a file inside the directory is
// modified. It might also be very expensive to stat files in such
// a case, so we only record the modification time of the directory.
let image_dir_change = match image_dir.metadata().and_then(|m| m.modified()) {
Ok(modified) => modified.into(),
Err(error) => {
tracing::error!("Could not read modification time of image dir: {:#}", error);
continue
},
};
// Read all image files from the image_dir
let images: HashSet<_> = match image_dir.read_dir() {
Ok(images) => {
// flatten ignores io error while traversing the iterator
images.flatten().filter_map(|entry| {
let path = entry.path();
if path.extension() == Some(OsStr::new("jpg")) {
Some(path)
} else {
None
}
}).collect()
},
Err(error) => {
tracing::error!("Could not read images: {:#}", error);
continue
},
};
let update_image_metadata = if image_dir_change != last_image_dir_change {
tracing::debug!("Update image list because image dir was modified");
true
} else if images != last_images {
// TODO: Maybe clear removed files from the image cache here?
tracing::debug!("Update image list because list of images changed");
true
} else if last_forced_update.elapsed() > Duration::from_secs(60*60) {
last_forced_update = Instant::now();
tracing::debug!("Update image list because one hour elapsed");
true
} else {
false
};
// Update image list
if update_image_metadata {
let images = images.clone();
let image_metadata = task::spawn_blocking(move || {
read_image_metadata(&images)
}).await.unwrap_or_else(|error| {
tracing::error!("Could not read images due to panic: {:#}", error);
Vec::new()
});
tracing::debug!("{} images in the image list cache", image_metadata.len());
*image_metadata_cache.write().unwrap() = image_metadata;
}
last_images = images;
last_image_dir_change = image_dir_change;
}
}
#[derive(Debug, Serialize, Default, Clone)]
pub struct ImageInfo {
width: u32,
height: u32,
@ -101,26 +241,35 @@ 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(image_dir): State<ImageDir>,
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 = read_images(&image_dir)?;
let images = image_list_cache.read().unwrap();
// Encrypt image names
let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]);
let chacha = XChaCha20Poly1305::new(chacha_key);
let mut images = images.into_iter().map(|mut image_info| {
let mut images = images.iter().cloned().map(|mut image_info| {
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let mut ciphertext = chacha.encrypt(&nonce, image_info.name.as_bytes())?;
@ -132,18 +281,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,
@ -151,9 +304,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("/")
@ -165,6 +319,7 @@ async fn converted_image(
State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>,
session: ReadableSession,
State(cpu_task_limit): State<CpuTaskLimit>,
) -> Result<impl IntoResponse, AppError> {
session.get::<()>("logged_in")
.ok_or(anyhow!("Trying to load image while not logged in!"))
@ -195,7 +350,7 @@ async fn converted_image(
.ok();
let cached_buffer = if let Some(image_modified) = image_modified {
image_cache
.read().await
.read().unwrap()
.get(image_name)
.filter(|(cache_modified, _)| *cache_modified == image_modified)
.map(|(_, image_buffer)| image_buffer)
@ -208,6 +363,7 @@ async fn converted_image(
image_buffer
}
None => {
let _cpu_task_permit = cpu_task_limit.clone().acquire_owned().await?;
let image_buffer = task::spawn_blocking(move || {
convert_image(&image_path)
.with_context(|| format!("Could not convert image {:?}", image_path))
@ -216,7 +372,7 @@ async fn converted_image(
if let Some(image_modified) = image_modified {
tracing::debug!("Add {:?} ({}k) to cache", image_name, image_buffer.len() / 1024);
image_cache
.write().await
.write().unwrap()
.insert(image_name.to_owned(), (image_modified, image_buffer.clone()));
}
@ -300,28 +456,21 @@ fn fix_image_orientation(image: DynamicImage, exif: &Exif) -> DynamicImage {
}
}
fn read_images(directory: &Path) -> Result<Vec<ImageInfo>> {
fn read_image_metadata(images: &HashSet<PathBuf>) -> Vec<ImageInfo> {
let mut files = vec![];
let directory_iterator = directory
.read_dir()
.with_context(|| format!("Could not read files in directory {:?}", directory))?.flatten();
for file in directory_iterator {
let path = file.path();
if path.extension() == Some(OsStr::new("jpg")) {
let image_info = match read_image_info(&path) {
Ok(image_info) => image_info,
Err(error) => {
tracing::warn!("Skipping {:?} due to error: {:#}", path, error);
continue;
}
};
files.push(image_info);
}
for path in images {
let image_info = match read_image_info(path) {
Ok(image_info) => image_info,
Err(error) => {
tracing::warn!("Skipping {:?} due to error: {:#}", path, error);
continue;
}
};
files.push(image_info);
}
Ok(files)
files
}
fn extract_exif_string(field: &exif::Field) -> Option<String> {
@ -353,10 +502,13 @@ fn read_image_info(path: &Path) -> Result<ImageInfo> {
.and_then(extract_exif_string);
match (datetime_without_timezone, timezone) {
(Some(datetime), Some(timezone)) => (datetime + &timezone).parse().ok(),
(Some(datetime), None) => (datetime + "Z").parse().ok(),
(Some(datetime), Some(timezone)) => Some(datetime + &timezone),
(Some(datetime), None) => Some(datetime + "+00:00"),
_ => None,
}
}.and_then(|datetime| {
DateTime::parse_from_str(&datetime, "%Y:%m:%d %T%:z")
.ok().map(|d| d.with_timezone(&Utc))
})
}).or_else(|| {
// If that doesn't work, fall back to the file modification time
std::fs::metadata(path)

View File

@ -7,6 +7,14 @@
padding: 0.7rem;
margin: 0;
}
header {
font-size: 1.3rem;
padding: 1rem;
margin-bottom: 1rem;
background-color: #ebff7a;
border: 3px solid #b0cf00;
border-radius: 1rem;
}
main {
display: flex;
flex-wrap: wrap;
@ -42,10 +50,15 @@
{% endblock head %}
{% block main %}
{% if top_message %}
<header>
{{top_message}}
</header>
{% endif %}
<main>
{% for image in images %}
<div style="aspect-ratio: {{image.width}} / {{image.height}}; flex-grow: {{10*image.width/image.height}};">
<img loading="lazy" src="/image/{{image.encrypted_name}}" alt="YOU SHALL NOT SAVE THIS IMAGE!" />
<img loading="lazy" src="/image/{{image.encrypted_name}}" alt="Could not load image" />
</div>
{% endfor %}
</main>