From b2a7787e45c0fa092ff9481cbe5247061c626b7f Mon Sep 17 00:00:00 2001 From: stefiosif Date: Sun, 7 Jun 2026 19:53:19 +0300 Subject: [PATCH] feat(backend): serve video with HTTP range requests --- backend/src/model.rs | 19 ++++++++ backend/src/web/routes_file.rs | 47 ++++++++++++++++++- .../src/lib/components/FilePreview.svelte | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/backend/src/model.rs b/backend/src/model.rs index b617217..597f49c 100644 --- a/backend/src/model.rs +++ b/backend/src/model.rs @@ -120,6 +120,25 @@ impl FileRepository { Ok(stream) } + pub async fn stream_part( + &self, + file_id: i64, + user_id: i64, + from: u64, + to: u64, + ) -> Result> + use<>, LoftError> { + info!( + "Fetching metadata of file \"{}\" from file_records", + file_id + ); + let record = self.get_file(file_id, user_id).await?; + info!("Streaming chunk of file \"{}\"", file_id); + let reader = self.op.reader(&record.storage_key).await.unwrap(); + let stream = reader.into_bytes_stream(from..to).await.unwrap(); + + Ok(stream) + } + pub async fn get_file(&self, file_id: i64, user_id: i64) -> Result { info!( "Fetching metadata of file \"{}\" from file_records", diff --git a/backend/src/web/routes_file.rs b/backend/src/web/routes_file.rs index 7c11231..31f2de4 100644 --- a/backend/src/web/routes_file.rs +++ b/backend/src/web/routes_file.rs @@ -2,6 +2,7 @@ use axum::{ Json, Router, body::Body, extract::{Multipart, Path, State}, + http::{HeaderMap, Response, StatusCode, header}, response::IntoResponse, routing::get, }; @@ -19,6 +20,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router { .route("/files", get(list_files).post(upload_file)) .route("/files/{id}", get(get_file).delete(delete_file)) .route("/files/{id}/download", get(download_file)) + .route("/files/{id}/stream_part", get(stream_part)) .with_state(file_repository) } @@ -54,7 +56,6 @@ async fn upload_file( Err(LoftError::UndefinedErrorType) } -#[axum::debug_handler] async fn get_file( State(file_repository): State, ctx: Ctx, @@ -66,7 +67,6 @@ async fn get_file( Ok(Json(record)) } -#[axum::debug_handler] async fn download_file( State(file_repository): State, ctx: Ctx, @@ -78,6 +78,49 @@ async fn download_file( Ok(Body::from_stream(stream)) } +async fn stream_part( + State(file_repository): State, + ctx: Ctx, + headers: HeaderMap, + Path(file_id): Path, +) -> Result { + info!("stream_part"); + let file_record = file_repository + .get_file(file_id as i64, ctx.user_id()) + .await?; + let file_size = file_record.size as u64; + let (start, end): (u64, u64) = parse_range(&headers, file_size); + + let stream = file_repository + .stream_part(file_id as i64, ctx.user_id(), start, end + 1) + .await?; + + let respones = Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header(header::CONTENT_TYPE, file_record.file_type) + .header(header::CONTENT_LENGTH, end - start + 1) + .header( + header::CONTENT_RANGE, + format!("bytes {start}-{end}/{file_size}"), + ) + .header(header::ACCEPT_RANGES, "bytes") + .body(Body::from_stream(stream)) + .unwrap(); + + Ok(respones) +} + +fn parse_range(headers: &HeaderMap, file_size: u64) -> (u64, u64) { + let range = headers.get(header::RANGE); + let str = range.unwrap().to_str().unwrap(); + let strip = str.strip_prefix("bytes=").unwrap(); + let tuple = strip.split_once("-").unwrap(); + let start = tuple.0.parse::().unwrap(); + let end = tuple.1.parse::().unwrap_or(file_size - 1); + + (start, end) +} + async fn delete_file( State(file_repository): State, ctx: Ctx, diff --git a/frontend/src/lib/components/FilePreview.svelte b/frontend/src/lib/components/FilePreview.svelte index 6541743..aead4a7 100644 --- a/frontend/src/lib/components/FilePreview.svelte +++ b/frontend/src/lib/components/FilePreview.svelte @@ -25,7 +25,7 @@ onclick={(e) => e.stopPropagation()} > {#if isVideo} - +