synops/maskinrommet/src/serving.rs
vegard 9566ba8dfe Fullfør oppgave 6.3: CAS-serving med GET /cas/{hash}
Nytt endepunkt streamer CAS-filer fra disk med riktig Content-Type
(oppslått fra media-nodens metadata i PG) og Cache-Control: immutable.
Hash-validering (64 hex-tegn) hindrer path traversal.
Tokio-streaming for effektiv håndtering av store filer.

Docker-compose oppdatert med CAS-volum for maskinrommet-containeren.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:58:21 +01:00

73 lines
2.3 KiB
Rust

//! 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<AppState>,
Path(hash): Path<String>,
) -> Result<Response, StatusCode> {
// 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
})?;
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())
}