//! CAS-serving: GET /cas/{hash} → stream fil fra disk. //! //! Slår opp MIME-type fra media-nodens metadata i PG. //! Streamer filen med riktig Content-Type og Content-Length. //! Hashen er i praksis en capability token — ingen auth kreves. use axum::{ body::Body, extract::{Path, State}, http::{header, StatusCode}, response::Response, }; use tokio::fs::File; use tokio_util::io::ReaderStream; use crate::AppState; /// Valider at hash er 64 hex-tegn (SHA-256). fn is_valid_hash(hash: &str) -> bool { hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) } /// GET /cas/{hash} — stream CAS-fil med riktig Content-Type. pub async fn get_cas_file( State(state): State, Path(hash): Path, ) -> Result { // Valider hash-format for å hindre path traversal if !is_valid_hash(&hash) { return Err(StatusCode::BAD_REQUEST); } // Sjekk at filen finnes i CAS let path = state.cas.path_for(&hash); if !path.exists() { return Err(StatusCode::NOT_FOUND); } // Slå opp MIME-type fra media-nodens metadata i PG let mime: String = sqlx::query_scalar( "SELECT metadata->>'mime' FROM nodes WHERE metadata->>'cas_hash' = $1 LIMIT 1", ) .bind(&hash) .fetch_optional(&state.db) .await .map_err(|e| { tracing::error!(hash = %hash, error = %e, "Feil ved MIME-oppslag i PG"); StatusCode::INTERNAL_SERVER_ERROR })? .unwrap_or_else(|| "application/octet-stream".to_string()); // Hent filstørrelse let metadata = tokio::fs::metadata(&path).await.map_err(|e| { tracing::error!(hash = %hash, error = %e, "Kunne ikke lese filmetadata"); StatusCode::INTERNAL_SERVER_ERROR })?; // Åpne fil og stream let file = File::open(&path).await.map_err(|e| { tracing::error!(hash = %hash, error = %e, "Kunne ikke åpne CAS-fil"); StatusCode::INTERNAL_SERVER_ERROR })?; // Oppdater last_accessed_at asynkront (fire-and-forget for ytelse) let db_clone = state.db.clone(); let hash_clone = hash.clone(); tokio::spawn(async move { crate::pruning::touch_access(&db_clone, &hash_clone).await; }); let stream = ReaderStream::new(file); let body = Body::from_stream(stream); Ok(Response::builder() .header(header::CONTENT_TYPE, mime) .header(header::CONTENT_LENGTH, metadata.len()) .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") .body(body) .unwrap()) }