Foundations for the v0.16 features. No new endpoints here — those land in
the next commit on top of these.
- migrations 008 + 009: commit the load-bearing compression_status column
that was uncommitted on disk; add 009_feature_toggles seeding the master
+ per-endpoint rate-limit switches, the master + per-area quota switches,
and the admin-editable privacy_note.
- services/config.rs (new): get_str / get_i64 / get_usize / get_f64 / get_bool
consolidating the scattered helpers that lived in three handlers.
- services/maintenance.rs (new):
- startup_recovery() — resets compression_status='processing' and
export_job.status='running' rows orphaned by a previous crashed
instance, so users never see permanent "Wird vorbereitet…" spinners.
- spawn_periodic_tasks() — hourly cleanup of expired sessions (rows
were never pruned) + rate-limiter HashMap pruning (windows kept one
entry per IP forever).
- services/jobs.rs (new sketch): BackgroundJob trait + JobContext for
future jobs to plug into the same progress + SSE pipeline as
compression/export. Not wired yet — codifies the convention.
- services/compression.rs: 120s hard timeout + kill_on_drop on ffmpeg
so a malformed video can't hang and leak a worker semaphore permit.
- services/rate_limiter.rs: new prune() called from the periodic task.
- state.rs: SseEvent::new() constructor so event-type strings stay
consistent instead of being typed inline at every emit site.
- models/user.rs: UserRole::as_str() for /me/context serialization.
- models/upload.rs: soft_delete() now runs in a transaction and
decrements the uploader's total_upload_bytes (GREATEST(0, …) guard) —
fixes a quota drift where deleting reclaimed no quota.
- Cargo.toml + Cargo.lock: add `infer = "0.15"` (multipart MIME sniffing
used by the upload handler).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.9 KiB
Rust
50 lines
1.9 KiB
Rust
//! Reads of the runtime-tunable `config` table.
|
|
//!
|
|
//! Each handler used to keep a small local copy of these helpers; consolidating them
|
|
//! here means one place to add a parser, one place to mock for tests, and one place to
|
|
//! find when a key changes. New keys do not require code changes — they're picked up
|
|
//! the next time someone calls `get_*`.
|
|
//!
|
|
//! Values are read with a default fallback so the app still starts if a key is missing
|
|
//! (e.g. during a migration window). Production seeds keys via migrations 005 and 009.
|
|
|
|
use sqlx::PgPool;
|
|
|
|
async fn fetch_raw(pool: &PgPool, key: &str) -> Option<String> {
|
|
sqlx::query_as::<_, (String,)>("SELECT value FROM config WHERE key = $1")
|
|
.bind(key)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|(v,)| v)
|
|
}
|
|
|
|
pub async fn get_str(pool: &PgPool, key: &str, default: &str) -> String {
|
|
fetch_raw(pool, key).await.unwrap_or_else(|| default.to_string())
|
|
}
|
|
|
|
pub async fn get_i64(pool: &PgPool, key: &str, default: i64) -> i64 {
|
|
fetch_raw(pool, key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
|
}
|
|
|
|
pub async fn get_usize(pool: &PgPool, key: &str, default: usize) -> usize {
|
|
fetch_raw(pool, key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
|
}
|
|
|
|
pub async fn get_f64(pool: &PgPool, key: &str, default: f64) -> f64 {
|
|
fetch_raw(pool, key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
|
}
|
|
|
|
/// Parses common truthy spellings used by both the migration seeds and the admin form.
|
|
/// Accepts `true/false`, `1/0`, `yes/no`, `on/off` — case-insensitive. Anything else
|
|
/// returns `default`.
|
|
pub async fn get_bool(pool: &PgPool, key: &str, default: bool) -> bool {
|
|
let Some(raw) = fetch_raw(pool, key).await else { return default };
|
|
match raw.trim().to_ascii_lowercase().as_str() {
|
|
"true" | "1" | "yes" | "on" => true,
|
|
"false" | "0" | "no" | "off" => false,
|
|
_ => default,
|
|
}
|
|
}
|