style(backend): rustfmt the whole tree; gate cargo fmt --check in CI

The backend had never been run through rustfmt. Doing it in one mechanical pass (134 files)
so no future functional diff is buried under formatting churn, then gating `cargo fmt
--check` in checks.yml so it stays clean.

Formatting only — no logic, SQL, or behaviour changed. Verified after the reformat:
cargo test 56 passed, clippy --all-targets -D warnings clean, cargo fmt --check clean.

This is the deferred cleanup noted when CI's Format step was first left out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-15 19:52:17 +02:00
parent 0fa40ddf80
commit ee554e7f38
29 changed files with 589 additions and 306 deletions

View File

@@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::time::Duration;
use axum::Json;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::Json;
use serde::{Deserialize, Serialize};
use crate::auth::middleware::RequireAdmin;
@@ -45,19 +45,17 @@ pub async fn get_stats(
.await?
.ok_or_else(|| AppError::NotFound("Event nicht gefunden.".into()))?;
let (user_count,): (i64,) =
sqlx::query_as("SELECT COUNT(*) FROM \"user\" WHERE event_id = $1")
let (user_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM \"user\" WHERE event_id = $1")
.bind(event.id)
.fetch_one(&state.pool)
.await?;
let (upload_count,): (i64,) =
sqlx::query_as("SELECT COUNT(*) FROM upload WHERE event_id = $1 AND deleted_at IS NULL")
.bind(event.id)
.fetch_one(&state.pool)
.await?;
let (upload_count,): (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM upload WHERE event_id = $1 AND deleted_at IS NULL",
)
.bind(event.id)
.fetch_one(&state.pool)
.await?;
let (comment_count,): (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM comment c
JOIN upload u ON u.id = c.upload_id
@@ -90,10 +88,9 @@ pub async fn get_config(
State(state): State<AppState>,
RequireAdmin(_auth): RequireAdmin,
) -> Result<Json<HashMap<String, String>>, AppError> {
let rows: Vec<(String, String)> =
sqlx::query_as("SELECT key, value FROM config ORDER BY key")
.fetch_all(&state.pool)
.await?;
let rows: Vec<(String, String)> = sqlx::query_as("SELECT key, value FROM config ORDER BY key")
.fetch_all(&state.pool)
.await?;
Ok(Json(rows.into_iter().collect()))
}
@@ -159,7 +156,7 @@ pub async fn patch_config(
None => {
return Err(AppError::BadRequest(format!(
"Ungültiger Wert für {key}: muss eine Zahl sein."
)))
)));
}
};
if integer_only && n.fract() != 0.0 {
@@ -297,7 +294,8 @@ pub async fn download_zip(
authenticate_download_ticket(&state, &q.ticket).await?;
enforce_export_rate(&state, &headers).await?;
let path = resolve_export_file(&state, "zip", "Der ZIP-Export ist noch nicht verfügbar.").await?;
let path =
resolve_export_file(&state, "zip", "Der ZIP-Export ist noch nicht verfügbar.").await?;
serve_file(path, "Gallery.zip", "application/zip").await
}
@@ -361,7 +359,7 @@ async fn serve_file(
content_type: &str,
) -> Result<axum::response::Response, AppError> {
use axum::body::Body;
use axum::http::{header, Response, StatusCode};
use axum::http::{Response, StatusCode, header};
use tokio_util::io::ReaderStream;
let file = tokio::fs::File::open(&path)
@@ -418,9 +416,7 @@ pub async fn export_status(
let job_status = |type_name: &str| {
jobs.iter()
.find(|(t, _, _)| t == type_name)
.map(|(_, status, pct)| {
serde_json::json!({ "status": status, "progress_pct": pct })
})
.map(|(_, status, pct)| serde_json::json!({ "status": status, "progress_pct": pct }))
.unwrap_or_else(|| serde_json::json!({ "status": "locked", "progress_pct": 0 }))
};

View File

@@ -1,8 +1,8 @@
use std::time::Duration;
use axum::Json;
use axum::extract::{Query, State};
use axum::http::HeaderMap;
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -130,7 +130,11 @@ pub async fn feed(
let has_more = rows.len() as i64 > limit;
let rows: Vec<FeedRow> = rows.into_iter().take(limit as usize).collect();
let next_cursor = if has_more { rows.last().map(|r| r.id) } else { None };
let next_cursor = if has_more {
rows.last().map(|r| r.id)
} else {
None
};
// Batch check which uploads the current user has liked
let upload_ids: Vec<Uuid> = rows.iter().map(|r| r.id).collect();
@@ -327,12 +331,11 @@ pub async fn hashtags(
State(state): State<AppState>,
auth: AuthUser,
) -> Result<Json<Vec<HashtagCount>>, AppError> {
let rows: Vec<(String, i64)> = sqlx::query_as(
"SELECT tag, upload_count FROM v_hashtag_counts WHERE event_id = $1",
)
.bind(auth.event_id)
.fetch_all(&state.pool)
.await?;
let rows: Vec<(String, i64)> =
sqlx::query_as("SELECT tag, upload_count FROM v_hashtag_counts WHERE event_id = $1")
.bind(auth.event_id)
.fetch_all(&state.pool)
.await?;
Ok(Json(
rows.into_iter()
@@ -362,14 +365,13 @@ async fn get_liked_set(
if upload_ids.is_empty() {
return std::collections::HashSet::new();
}
let rows: Vec<(Uuid,)> = sqlx::query_as(
"SELECT upload_id FROM \"like\" WHERE user_id = $1 AND upload_id = ANY($2)",
)
.bind(user_id)
.bind(upload_ids)
.fetch_all(pool)
.await
.unwrap_or_default();
let rows: Vec<(Uuid,)> =
sqlx::query_as("SELECT upload_id FROM \"like\" WHERE user_id = $1 AND upload_id = ANY($2)")
.bind(user_id)
.bind(upload_ids)
.fetch_all(pool)
.await
.unwrap_or_default();
rows.into_iter().map(|r| r.0).collect()
}

View File

@@ -1,6 +1,6 @@
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -57,7 +57,6 @@ async fn remaining_operators(
Ok(count)
}
#[derive(Deserialize)]
pub struct SetRoleRequest {
pub role: String,
@@ -115,7 +114,9 @@ pub async fn ban_user(
// The ban request carries no body — ban always hides (no per-request options).
// Cannot ban yourself or another host/admin
if user_id == auth.user_id {
return Err(AppError::BadRequest("Du kannst dich nicht selbst sperren.".into()));
return Err(AppError::BadRequest(
"Du kannst dich nicht selbst sperren.".into(),
));
}
let target = sqlx::query_as::<_, (String,)>(
"SELECT role::text FROM \"user\" WHERE id = $1 AND event_id = $2",
@@ -126,8 +127,12 @@ pub async fn ban_user(
.await?
.ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?;
if target.0 == "admin" || (target.0 == "host" && auth.role != crate::models::user::UserRole::Admin) {
return Err(AppError::Forbidden("Du kannst diesen Benutzer nicht sperren.".into()));
if target.0 == "admin"
|| (target.0 == "host" && auth.role != crate::models::user::UserRole::Admin)
{
return Err(AppError::Forbidden(
"Du kannst diesen Benutzer nicht sperren.".into(),
));
}
// Floor: never leave the event with zero operators. Banning removes the target from
@@ -166,9 +171,12 @@ pub async fn ban_user(
// because it is the copy people keep. The export already filters `is_banned = FALSE`, so a
// FUTURE export excludes them; without this, an ALREADY-RELEASED archive would keep serving a
// banned user's photos forever. Same class as a takedown, so same treatment.
let regen =
crate::services::export::invalidate_and_arm(&mut tx, &state.config.event_slug, Affects::Both)
.await?;
let regen = crate::services::export::invalidate_and_arm(
&mut tx,
&state.config.event_slug,
Affects::Both,
)
.await?;
tx.commit().await?;
if let Some(r) = regen {
start_regen(&state, r);
@@ -237,9 +245,12 @@ pub async fn unban_user(
// The mirror of the ban case: an unban RESTORES their uploads to the export query, so an
// already-released keepsake is now missing content it should contain. Rebuild it.
let regen =
crate::services::export::invalidate_and_arm(&mut tx, &state.config.event_slug, Affects::Both)
.await?;
let regen = crate::services::export::invalidate_and_arm(
&mut tx,
&state.config.event_slug,
Affects::Both,
)
.await?;
tx.commit().await?;
if let Some(r) = regen {
start_regen(&state, r);
@@ -266,9 +277,12 @@ pub async fn rebuild_export(
RequireHost(auth): RequireHost,
) -> Result<StatusCode, AppError> {
let mut tx = state.pool.begin().await?;
let regen =
crate::services::export::invalidate_and_arm(&mut tx, &state.config.event_slug, Affects::Both)
.await?;
let regen = crate::services::export::invalidate_and_arm(
&mut tx,
&state.config.event_slug,
Affects::Both,
)
.await?;
tx.commit().await?;
let Some(r) = regen else {
@@ -316,7 +330,7 @@ pub async fn set_role(
_ => {
return Err(AppError::BadRequest(
"Ungültige Rolle. Erlaubt: guest, host.".into(),
))
));
}
};
@@ -413,13 +427,12 @@ pub async fn reset_user_pin(
_ => {
return Err(AppError::Forbidden(
"Du darfst die PIN dieses Benutzers nicht zurücksetzen.".into(),
))
));
}
}
let pin: String = format!("{:04}", rand::rng().random_range(0..10000u32));
let pin_hash =
bcrypt::hash(&pin, 12).map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?;
let pin_hash = bcrypt::hash(&pin, 12).map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?;
sqlx::query(
"UPDATE \"user\"
@@ -564,9 +577,12 @@ pub async fn host_delete_upload(
if !deleted {
return Err(AppError::NotFound("Upload nicht gefunden.".into()));
}
let regen =
crate::services::export::invalidate_and_arm(&mut tx, &state.config.event_slug, Affects::Both)
.await?;
let regen = crate::services::export::invalidate_and_arm(
&mut tx,
&state.config.event_slug,
Affects::Both,
)
.await?;
tx.commit().await?;
let _ = state.sse_tx.send(SseEvent::new(

View File

@@ -7,8 +7,8 @@
//! account page loads this once on mount instead of issuing several round trips.
//! - `GET /api/v1/me/quota` — live per-user storage quota estimate.
use axum::extract::State;
use axum::Json;
use axum::extract::State;
use serde::Serialize;
use crate::auth::middleware::AuthUser;
@@ -73,12 +73,19 @@ pub async fn get_context(
let privacy_note = config::get_str(&state.config_cache, "privacy_note", "").await;
let quota_enabled = config::get_bool(&state.config_cache, "quota_enabled", true).await;
let storage_quota_enabled = config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
let storage_quota_enabled =
config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
let event = crate::models::event::Event::find_by_slug(&state.pool, &state.config.event_slug)
.await?;
let uploads_locked = event.as_ref().map(|e| e.uploads_locked_at.is_some()).unwrap_or(false);
let gallery_released = event.as_ref().map(|e| e.export_released_at.is_some()).unwrap_or(false);
let event =
crate::models::event::Event::find_by_slug(&state.pool, &state.config.event_slug).await?;
let uploads_locked = event
.as_ref()
.map(|e| e.uploads_locked_at.is_some())
.unwrap_or(false);
let gallery_released = event
.as_ref()
.map(|e| e.export_released_at.is_some())
.unwrap_or(false);
Ok(Json(MeContextDto {
user_id: user.id,

View File

@@ -1,7 +1,7 @@
//! Unauthenticated, read-only endpoints safe to expose before a user has joined.
use axum::extract::State;
use axum::Json;
use axum::extract::State;
use serde::Serialize;
use crate::state::AppState;

View File

@@ -1,6 +1,6 @@
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

View File

@@ -1,14 +1,14 @@
use std::convert::Infallible;
use std::time::Duration;
use axum::Json;
use axum::extract::{Query, State};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::Json;
use futures::stream::Stream;
use serde::{Deserialize, Serialize};
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use crate::auth::middleware::AuthUser;
use crate::error::AppError;
@@ -42,7 +42,10 @@ pub async fn issue_ticket(
let server_time = sqlx::query_scalar("SELECT NOW()")
.fetch_one(&state.pool)
.await?;
Ok(Json(StreamTicketResponse { ticket, server_time }))
Ok(Json(StreamTicketResponse {
ticket,
server_time,
}))
}
/// SSE stream endpoint. Authenticates via a single-use ticket (see

View File

@@ -1,8 +1,8 @@
use std::time::Duration;
use axum::Json;
use axum::extract::{Multipart, Path, State};
use axum::http::StatusCode;
use axum::Json;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use uuid::Uuid;
@@ -47,7 +47,8 @@ pub async fn upload(
let rate_limits_on = config::get_bool(&state.config_cache, "rate_limits_enabled", true).await;
let upload_rate_on = config::get_bool(&state.config_cache, "upload_rate_enabled", true).await;
if rate_limits_on && upload_rate_on {
let upload_rate = config::get_i64(&state.config_cache, "upload_rate_per_hour", 10).await as usize;
let upload_rate =
config::get_i64(&state.config_cache, "upload_rate_per_hour", 10).await as usize;
if let Err(retry_after_secs) = state.rate_limiter.check_with_retry(
format!("upload:{}", auth.user_id),
upload_rate,
@@ -101,7 +102,10 @@ pub async fn upload(
// On success the temp file is renamed into place under its detected extension.
let upload_id = Uuid::new_v4();
let event_slug = &state.config.event_slug;
let originals_dir = state.config.media_path.join(format!("originals/{event_slug}"));
let originals_dir = state
.config
.media_path
.join(format!("originals/{event_slug}"));
let temp_abs = originals_dir.join(format!("{upload_id}.tmp"));
let mut streamed: Option<(i64, Vec<u8>)> = None; // (size, head bytes for sniffing)
@@ -139,12 +143,20 @@ pub async fn upload(
streamed = Some(stream_field_to_file(field, &temp_abs, cap_bytes).await?);
}
"caption" => {
caption =
Some(field.text().await.map_err(|e| AppError::BadRequest(e.to_string()))?);
caption = Some(
field
.text()
.await
.map_err(|e| AppError::BadRequest(e.to_string()))?,
);
}
"hashtags" => {
hashtags_csv =
Some(field.text().await.map_err(|e| AppError::BadRequest(e.to_string()))?);
hashtags_csv = Some(
field
.text()
.await
.map_err(|e| AppError::BadRequest(e.to_string()))?,
);
}
_ => {}
}
@@ -225,7 +237,8 @@ pub async fn upload(
// number of active uploaders. Gated by master + per-area toggles so the admin can
// disable it on trusted instances.
let quota_on = config::get_bool(&state.config_cache, "quota_enabled", true).await;
let storage_quota_on = config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
let storage_quota_on =
config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
// When quota is enforced, this holds the byte ceiling so the increment UPDATE below can
// enforce it atomically (`WHERE total + size <= limit`). Without that guard, two
// concurrent uploads from the same user (e.g. phone + laptop) both pass this stale
@@ -321,11 +334,13 @@ pub async fn upload(
.execute(&mut *tx)
.await?
} else {
sqlx::query("UPDATE \"user\" SET total_upload_bytes = total_upload_bytes + $2 WHERE id = $1")
.bind(auth.user_id)
.bind(size)
.execute(&mut *tx)
.await?
sqlx::query(
"UPDATE \"user\" SET total_upload_bytes = total_upload_bytes + $2 WHERE id = $1",
)
.bind(auth.user_id)
.bind(size)
.execute(&mut *tx)
.await?
};
if inc.rows_affected() == 0 {
return Err(AppError::QuotaExceeded(
@@ -592,15 +607,15 @@ fn quota_limit_bytes(free_disk: i64, tolerance: f64, active_uploaders: i64) -> i
/// check (upload handler) or hide the UI (quota endpoint).
pub async fn compute_storage_quota(state: &AppState) -> QuotaEstimate {
let quota_on = config::get_bool(&state.config_cache, "quota_enabled", true).await;
let storage_quota_on = config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
let storage_quota_on =
config::get_bool(&state.config_cache, "storage_quota_enabled", true).await;
let tolerance = config::get_f64(&state.config_cache, "quota_tolerance", 0.75).await;
let (active_count,): (i64,) = sqlx::query_as(
"SELECT COUNT(DISTINCT user_id) FROM upload WHERE deleted_at IS NULL",
)
.fetch_one(&state.pool)
.await
.unwrap_or((0,));
let (active_count,): (i64,) =
sqlx::query_as("SELECT COUNT(DISTINCT user_id) FROM upload WHERE deleted_at IS NULL")
.fetch_one(&state.pool)
.await
.unwrap_or((0,));
let active = active_count.max(1);
// Cached disk reading. `None` means we couldn't resolve the media filesystem.
@@ -644,7 +659,7 @@ async fn stream_media_file(
cache_control: &str,
) -> Result<axum::response::Response, AppError> {
use axum::body::Body;
use axum::http::{header, Response, StatusCode};
use axum::http::{Response, StatusCode, header};
use tokio_util::io::ReaderStream;
if !absolute.exists() {
@@ -723,7 +738,13 @@ pub async fn get_preview(
.preview_path
.ok_or_else(|| AppError::NotFound("Vorschau nicht verfügbar.".into()))?;
let absolute = state.config.media_path.join(&rel);
stream_media_file(&absolute, "image/jpeg".to_string(), "inline", "private, max-age=300").await
stream_media_file(
&absolute,
"image/jpeg".to_string(),
"inline",
"private, max-age=300",
)
.await
}
/// Streaming access to an upload's **thumbnail** (video poster). Gated identically to
@@ -739,7 +760,13 @@ pub async fn get_thumbnail(
.thumbnail_path
.ok_or_else(|| AppError::NotFound("Thumbnail nicht verfügbar.".into()))?;
let absolute = state.config.media_path.join(&rel);
stream_media_file(&absolute, "image/jpeg".to_string(), "inline", "private, max-age=300").await
stream_media_file(
&absolute,
"image/jpeg".to_string(),
"inline",
"private, max-age=300",
)
.await
}
#[cfg(test)]