Compare commits
10 Commits
a7ade951aa
...
9c319d6949
Author | SHA1 | Date |
---|---|---|
Klemens Schölhorn | 9c319d6949 | |
Klemens Schölhorn | 54295ea098 | |
Klemens Schölhorn | 48a3a206a3 | |
Klemens Schölhorn | 68c7fd2ac4 | |
Klemens Schölhorn | 17d0bae1aa | |
Klemens Schölhorn | 03ab64ddbc | |
Klemens Schölhorn | 8f004e5e65 | |
Klemens Schölhorn | 68f50b5292 | |
Klemens Schölhorn | 9af4831efa | |
Klemens Schölhorn | 6206d0ea58 |
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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)?)
|
||||
}
|
250
src/main.rs
250
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, 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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue