use crate::storage::{FileStorage, StorageError, StorageResult};
use axum::{
body::Body,
extract::{Path, State},
http::{
header::{
ACCEPT_RANGES, CACHE_CONTROL, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG,
IF_NONE_MATCH, IF_RANGE, LAST_MODIFIED, RANGE,
},
HeaderMap, HeaderValue, StatusCode,
},
response::{IntoResponse, Response},
};
use std::{
future::Future,
pin::Pin,
sync::Arc,
time::SystemTime,
};
pub type FileAccessControl = Arc<
dyn Fn(Option<String>, String) -> Pin<Box<dyn Future<Output = StorageResult<bool>> + Send>>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct FileServingMiddleware<S: FileStorage> {
#[allow(dead_code)] storage: Arc<S>,
#[allow(dead_code)] access_control: Option<FileAccessControl>,
#[allow(dead_code)] cache_max_age: u32,
#[allow(dead_code)] enable_cdn_headers: bool,
}
impl<S: FileStorage> FileServingMiddleware<S> {
#[must_use]
pub fn new(storage: Arc<S>) -> Self {
Self {
storage,
access_control: None,
cache_max_age: 86400, enable_cdn_headers: false,
}
}
#[must_use]
pub fn with_access_control(mut self, access_control: FileAccessControl) -> Self {
self.access_control = Some(access_control);
self
}
#[must_use]
pub const fn with_cache_max_age(mut self, seconds: u32) -> Self {
self.cache_max_age = seconds;
self
}
#[must_use]
pub const fn with_cdn_headers(mut self) -> Self {
self.enable_cdn_headers = true;
self
}
}
pub async fn serve_file<S: FileStorage>(
State(storage): State<Arc<S>>,
Path(file_id): Path<String>,
headers: HeaderMap,
) -> Result<Response, FileServingError> {
let metadata = storage
.get_metadata(&file_id)
.await
.map_err(FileServingError::Storage)?;
let data = storage
.retrieve(&file_id)
.await
.map_err(FileServingError::Storage)?;
let etag = format!(r#""{}-{}""#, file_id, data.len());
let content_type = if !metadata.content_type.is_empty()
&& metadata.content_type != "application/octet-stream"
{
metadata.content_type
} else {
mime_guess::from_path(&metadata.filename)
.first_or_octet_stream()
.to_string()
};
if let Some(if_none_match) = headers.get(IF_NONE_MATCH) {
if if_none_match.to_str().is_ok_and(|v| v == etag) {
return Ok((StatusCode::NOT_MODIFIED, ()).into_response());
}
}
if let Some(range_header) = headers.get(RANGE) {
return serve_range_request(&data, range_header, &etag, &content_type, &headers);
}
Ok(build_file_response(data, &etag, &content_type, None))
}
fn serve_range_request(
data: &[u8],
range_header: &HeaderValue,
etag: &str,
content_type: &str,
headers: &HeaderMap,
) -> Result<Response, FileServingError> {
let file_size = data.len();
if let Some(if_range) = headers.get(IF_RANGE) {
if if_range.to_str().map_or(true, |v| v != etag) {
return Ok(build_file_response(data.to_vec(), etag, content_type, None));
}
}
let range_str = range_header
.to_str()
.map_err(|_| FileServingError::InvalidRange)?;
if !range_str.starts_with("bytes=") {
return Err(FileServingError::InvalidRange);
}
let range_spec = &range_str[6..]; let (start_str, end_str) = range_spec
.split_once('-')
.ok_or(FileServingError::InvalidRange)?;
let is_suffix_range = start_str.is_empty();
let start: usize = if is_suffix_range {
let suffix_len: usize = end_str
.parse()
.map_err(|_| FileServingError::InvalidRange)?;
file_size.saturating_sub(suffix_len)
} else {
start_str
.parse()
.map_err(|_| FileServingError::InvalidRange)?
};
let end: usize = if is_suffix_range {
file_size - 1
} else if end_str.is_empty() {
file_size - 1
} else {
end_str
.parse::<usize>()
.map_err(|_| FileServingError::InvalidRange)?
.min(file_size - 1)
};
if start > end || start >= file_size {
return Err(FileServingError::RangeNotSatisfiable(file_size));
}
let range_data = data[start..=end].to_vec();
let content_range = format!("bytes {start}-{end}/{file_size}");
Ok(build_file_response(
range_data,
etag,
content_type,
Some((&content_range, StatusCode::PARTIAL_CONTENT)),
))
}
fn build_file_response(
data: Vec<u8>,
etag: &str,
content_type: &str,
range_info: Option<(&str, StatusCode)>,
) -> Response {
let mut response = Response::builder();
let status = range_info.map_or(StatusCode::OK, |(_, code)| code);
response = response.status(status);
response = response
.header(CONTENT_TYPE, content_type)
.header(CONTENT_LENGTH, data.len())
.header(ETAG, etag)
.header(ACCEPT_RANGES, "bytes");
if let Some((content_range, _)) = range_info {
response = response.header(CONTENT_RANGE, content_range);
}
response = response
.header(CACHE_CONTROL, "public, max-age=86400")
.header(
LAST_MODIFIED,
httpdate::fmt_http_date(SystemTime::now()),
);
response
.body(Body::from(data))
.unwrap_or_else(|_| Response::new(Body::empty()))
}
#[derive(Debug)]
pub enum FileServingError {
Storage(StorageError),
InvalidRange,
RangeNotSatisfiable(usize),
AccessDenied,
}
impl std::fmt::Display for FileServingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Storage(e) => write!(f, "Storage error: {e}"),
Self::InvalidRange => write!(f, "Invalid range request"),
Self::RangeNotSatisfiable(size) => {
write!(f, "Range not satisfiable (file size: {size})")
}
Self::AccessDenied => write!(f, "Access denied"),
}
}
}
impl std::error::Error for FileServingError {}
impl IntoResponse for FileServingError {
fn into_response(self) -> Response {
let (status, message) = match self {
Self::Storage(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
Self::InvalidRange => (StatusCode::BAD_REQUEST, self.to_string()),
Self::RangeNotSatisfiable(size) => {
let response = Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{size}"))
.body(Body::from(self.to_string()))
.unwrap_or_else(|_| Response::new(Body::empty()));
return response;
}
Self::AccessDenied => (StatusCode::FORBIDDEN, self.to_string()),
};
(status, message).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{LocalFileStorage, UploadedFile};
use tempfile::TempDir;
#[test]
fn test_etag_generation() {
let file_id = "test-file-123";
let data = b"Hello, World!";
let etag = format!(r#""{}-{}""#, file_id, data.len());
assert_eq!(etag, r#""test-file-123-13""#);
}
#[tokio::test]
async fn test_serve_file_uses_stored_content_type() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let file = UploadedFile::new("document.pdf", "application/pdf", b"fake pdf".to_vec());
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(content_type, "application/pdf");
}
#[tokio::test]
async fn test_serve_file_uses_mime_guess_fallback() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let file = UploadedFile::new(
"image.png",
"application/octet-stream",
b"fake png".to_vec(),
);
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(content_type, "image/png");
}
#[tokio::test]
async fn test_serve_file_preserves_various_content_types() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let test_cases = vec![
("photo.jpg", "image/jpeg", "image/jpeg"),
("video.mp4", "video/mp4", "video/mp4"),
("data.json", "application/json", "application/json"),
("style.css", "text/css", "text/css"),
("script.js", "application/javascript", "application/javascript"),
];
for (filename, stored_type, expected_type) in test_cases {
let file = UploadedFile::new(filename, stored_type, b"test data".to_vec());
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(
content_type,
expected_type,
"Content-Type mismatch for {filename}"
);
}
}
#[tokio::test]
async fn test_serve_file_fallback_for_unknown_extension() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let file = UploadedFile::new(
"file.unknownext",
"application/octet-stream",
b"data".to_vec(),
);
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(content_type, "application/octet-stream");
}
#[tokio::test]
async fn test_range_request_full_range() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = (0_u8..=255).cycle().take(1000).collect::<Vec<u8>>();
let file = UploadedFile::new("test.bin", "application/octet-stream", data.clone());
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=100-199"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 100-199/1000");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "100");
assert!(response.headers().contains_key(ETAG));
assert_eq!(
response.headers().get(ACCEPT_RANGES).unwrap(),
"bytes"
);
}
#[tokio::test]
async fn test_range_request_suffix_range() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = (0_u8..=255).cycle().take(1000).collect::<Vec<u8>>();
let file = UploadedFile::new("test.bin", "application/octet-stream", data.clone());
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=-100"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 900-999/1000");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "100");
}
#[tokio::test]
async fn test_range_request_suffix_range_exceeds_file_size() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data.clone());
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=-500"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 0-99/100");
}
#[tokio::test]
async fn test_range_request_open_ended() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = (0_u8..=255).cycle().take(1000).collect::<Vec<u8>>();
let file = UploadedFile::new("test.bin", "application/octet-stream", data.clone());
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=800-"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 800-999/1000");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "200");
}
#[tokio::test]
async fn test_range_request_single_byte() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=50-50"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 50-50/100");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "1");
}
#[tokio::test]
async fn test_range_request_invalid_format_no_bytes_prefix() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("0-99"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
assert!(matches!(err, FileServingError::InvalidRange));
}
#[tokio::test]
async fn test_range_request_invalid_format_no_dash() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=50"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
assert!(matches!(err, FileServingError::InvalidRange));
}
#[tokio::test]
async fn test_range_request_invalid_non_numeric() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=abc-def"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
assert!(matches!(err, FileServingError::InvalidRange));
}
#[tokio::test]
async fn test_range_request_start_greater_than_end() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=50-20"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
assert!(matches!(err, FileServingError::RangeNotSatisfiable(100)));
}
#[tokio::test]
async fn test_range_request_start_beyond_file_size() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=100-199"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
assert!(matches!(err, FileServingError::RangeNotSatisfiable(100)));
}
#[tokio::test]
async fn test_range_request_end_exceeds_file_size() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=50-200"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 50-99/100");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "50");
}
#[tokio::test]
async fn test_range_request_with_if_range_matching_etag() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
let etag = response.headers().get(ETAG).unwrap().clone();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=0-49"));
headers.insert(IF_RANGE, etag);
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 0-49/100");
}
#[tokio::test]
async fn test_range_request_with_if_range_non_matching_etag() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=0-49"));
headers.insert(IF_RANGE, HeaderValue::from_static("\"wrong-etag\""));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(!response.headers().contains_key(CONTENT_RANGE));
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "100");
}
#[tokio::test]
async fn test_range_not_satisfiable_error_includes_content_range_header() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=200-299"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers).await;
assert!(response.is_err());
let err = response.unwrap_err();
let response = err.into_response();
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes */100");
}
#[tokio::test]
async fn test_range_request_preserves_cache_headers() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=0-49"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert!(response.headers().contains_key(ETAG));
assert!(response.headers().contains_key(CACHE_CONTROL));
assert!(response.headers().contains_key(LAST_MODIFIED));
}
#[tokio::test]
async fn test_no_range_header_serves_full_file() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let headers = HeaderMap::new();
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(!response.headers().contains_key(CONTENT_RANGE));
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "100");
assert_eq!(
response.headers().get(ACCEPT_RANGES).unwrap(),
"bytes"
);
}
#[tokio::test]
async fn test_range_request_first_byte() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=0-0"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 0-0/100");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "1");
}
#[tokio::test]
async fn test_range_request_last_byte() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=99-99"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 99-99/100");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "1");
}
#[tokio::test]
async fn test_range_request_entire_file_as_range() {
let temp = TempDir::new().unwrap();
let storage = Arc::new(LocalFileStorage::new(temp.path().to_path_buf()).unwrap());
let data = vec![42u8; 100];
let file = UploadedFile::new("test.bin", "application/octet-stream", data);
let stored = storage.store(file).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert(RANGE, HeaderValue::from_static("bytes=0-99"));
let response = serve_file(State(storage.clone()), Path(stored.id.clone()), headers)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_range = response.headers().get(CONTENT_RANGE).unwrap();
assert_eq!(content_range, "bytes 0-99/100");
let content_length = response.headers().get(CONTENT_LENGTH).unwrap();
assert_eq!(content_length, "100");
}
}