use std::{convert::Infallible, io};
use rama_core::bytes::Bytes;
use rama_core::telemetry::tracing;
use rama_core::{Service, error::BoxError};
use rama_http_headers::{AcceptRanges, ContentType, HeaderMapExt as _, HttpResponseBuilderExt};
use super::open_file::{FileOpened, OpenFileOutput};
use crate::headers::encoding::Encoding;
use crate::{
Body, HeaderValue, Request, Response, StatusCode, StreamingBody,
body::util::BodyExt,
header::{self, ALLOW},
service::fs::AsyncReadBody,
service::web::response::IntoResponse,
};
#[cfg(feature = "html")]
use crate::service::web::response::Html;
pub(super) async fn consume_open_file_result<ReqBody, ResBody, F>(
open_file_result: Result<OpenFileOutput, std::io::Error>,
fallback_and_request: Option<(&F, Request<ReqBody>)>,
) -> Result<Response, std::io::Error>
where
F: Service<Request<ReqBody>, Output = Response<ResBody>, Error = Infallible> + Clone,
ResBody: StreamingBody<Data = Bytes> + Send + Sync + 'static,
ResBody::Error: Into<BoxError>,
{
match open_file_result {
Ok(OpenFileOutput::FileOpened(file_output)) => Ok(build_response(*file_output)),
Ok(OpenFileOutput::Redirect { location }) => {
let mut res = response_with_status(StatusCode::TEMPORARY_REDIRECT);
res.headers_mut()
.insert(rama_http_types::header::LOCATION, location);
Ok(res)
}
#[cfg(feature = "html")]
Ok(OpenFileOutput::Html(payload)) => Ok(Html(payload).into_response()),
Ok(OpenFileOutput::FileNotFound | OpenFileOutput::InvalidFilename) => {
if let Some((fallback, request)) = fallback_and_request {
serve_fallback(fallback, request).await
} else {
Ok(not_found())
}
}
Ok(OpenFileOutput::PreconditionFailed) => {
Ok(response_with_status(StatusCode::PRECONDITION_FAILED))
}
Ok(OpenFileOutput::NotModified {
etag,
last_modified,
}) => {
let mut res = response_with_status(StatusCode::NOT_MODIFIED);
if let Some(etag) = etag {
res.headers_mut().typed_insert(etag);
}
if let Some(last_modified) = last_modified
&& let Ok(value) = HeaderValue::try_from(last_modified.0.to_string())
{
res.headers_mut().insert(header::LAST_MODIFIED, value);
}
Ok(res)
}
Err(err) => {
#[cfg(target_family = "unix")]
let error_is_not_a_directory = err.raw_os_error() == Some(20);
#[cfg(not(target_family = "unix"))]
let error_is_not_a_directory = false;
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) || error_is_not_a_directory
{
if let Some((fallback, request)) = fallback_and_request {
serve_fallback(fallback, request).await
} else {
Ok(not_found())
}
} else {
Err(err)
}
}
}
}
#[inline(always)]
pub(super) fn method_not_allowed() -> Response {
let mut res = response_with_status(StatusCode::METHOD_NOT_ALLOWED);
res.headers_mut()
.insert(ALLOW, HeaderValue::from_static("GET,HEAD"));
res
}
#[inline(always)]
fn response_with_status(status: StatusCode) -> Response {
status.into_response()
}
#[inline(always)]
pub(super) fn not_found() -> Response {
response_with_status(StatusCode::NOT_FOUND)
}
pub(super) async fn serve_fallback<F, B, FResBody>(
fallback: &F,
req: Request<B>,
) -> Result<Response, std::io::Error>
where
F: Service<Request<B>, Output = Response<FResBody>, Error = Infallible>,
FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
{
let response = fallback.serve(req).await.unwrap();
Ok(response
.map(|body| {
body.map_err(|err| match err.into().downcast::<io::Error>() {
Ok(err) => *err,
Err(err) => io::Error::other(err),
})
.boxed()
})
.map(Body::new))
}
fn build_response(output: FileOpened) -> Response {
let size = output.extent.file_size();
let mut builder = Response::builder()
.typed_header(ContentType::new(output.mime_value))
.typed_header(AcceptRanges::bytes());
if let Some(encoding) = output
.maybe_encoding
.filter(|encoding| *encoding != Encoding::Identity)
{
builder = builder.header(header::CONTENT_ENCODING, HeaderValue::from(encoding));
}
if output.precompression_configured {
builder = builder.header(header::VARY, HeaderValue::from_static("accept-encoding"));
}
if let Some(last_modified) = output.last_modified {
builder = builder.header(header::LAST_MODIFIED, last_modified.0.to_string());
}
if let Some(etag) = output.etag {
builder = builder.typed_header(etag);
}
match output.maybe_range {
Some(Ok(ranges)) => {
if let Some(range) = ranges.first() {
if ranges.len() > 1 {
builder
.header(header::CONTENT_RANGE, format!("bytes */{size}"))
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.body(Body::from(Bytes::from(
"Cannot serve multipart range requests",
)))
.unwrap_or_else(|err| {
tracing::debug!("failed to create RANGE_NOT_SATISFIABLE response: {err}; error 500 resp instead...");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
} else {
let range_size = range.end() - range.start() + 1;
let body = if let Some(reader) = output.extent.into_reader() {
Body::new(
AsyncReadBody::with_capacity_limited(
reader,
output.chunk_size,
range_size,
)
.boxed(),
)
} else {
Body::empty()
};
let content_length = if size == 0 {
0
} else {
range.end() - range.start() + 1
};
builder
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", range.start(), range.end(), size),
)
.header(header::CONTENT_LENGTH, content_length)
.status(StatusCode::PARTIAL_CONTENT)
.body(body)
.unwrap_or_else(|err| {
tracing::debug!("failed to create PARTIAL_CONTENT response: {err}; error 500 resp instead...");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
} else {
builder
.header(header::CONTENT_RANGE, format!("bytes */{size}"))
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.body(Body::from(Bytes::from(
"No range found after parsing range header, please file an issue",
)))
.unwrap_or_else(|err| {
tracing::debug!("failed to create RANGE_NOT_SATISFIABLE response: {err}; error 500 resp instead...");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
}
Some(Err(_)) => builder
.header(header::CONTENT_RANGE, format!("bytes */{size}"))
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.body(Body::empty())
.unwrap_or_else(|err| {
tracing::debug!("failed to create RANGE_NOT_SATISFIABLE response: {err}; error 500 resp instead...");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}),
None => {
let body = if let Some(reader) = output.extent.into_reader() {
Body::new(AsyncReadBody::with_capacity(reader, output.chunk_size).boxed())
} else {
Body::empty()
};
builder
.header(header::CONTENT_LENGTH, size)
.body(body)
.unwrap_or_else(|err| {
tracing::debug!("failed to create non-range content-length response: {err}; error 500 resp instead...");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
}
}