use super::{with_file, with_partial_file};
use super::{Caching, IntoPathBuf, Range};
use crate::error::ClientErrorKind;
use crate::header::{Method, RequestHeader, StatusCode};
use crate::into::{IntoResponse, IntoRoute};
use crate::routes::{check_static, Route};
use crate::util::PinnedFuture;
use crate::{Data, Error, Request, Response};
use std::borrow::Cow;
use std::io;
use std::path::Path;
use std::time::Duration;
pub async fn serve_file(
path: impl AsRef<Path>,
req: &Request,
caching: Option<Caching>,
) -> io::Result<Response> {
if matches!(&caching, Some(c) if c.if_none_match(req.header())) {
return Ok(caching.unwrap().into_response());
}
let range = Range::parse(req.header());
let mut res = match range {
Some(range) => with_partial_file(path, range).await?.into_response(),
None => with_file(path).await?.into_response(),
};
if let Some(caching) = caching {
if matches!(
res.header.status_code,
StatusCode::OK | StatusCode::NOT_FOUND
) {
caching.complete_header(&mut res.header);
}
}
Ok(res)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CachingBuilder {
None,
Default,
MaxAge(Duration),
}
impl From<CachingBuilder> for Option<Caching> {
fn from(b: CachingBuilder) -> Self {
match b {
CachingBuilder::None => None,
CachingBuilder::Default => Some(Caching::default()),
CachingBuilder::MaxAge(age) => Some(Caching::new(age)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticFiles {
uri: &'static str,
path: &'static str,
caching: CachingBuilder,
}
impl StaticFiles {
pub const fn new(uri: &'static str, path: &'static str) -> Self {
Self {
uri,
path,
caching: CachingBuilder::Default,
}
}
pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
Self {
uri,
path,
caching: CachingBuilder::None,
}
}
pub const fn cache_with_age(
uri: &'static str,
path: &'static str,
max_age: Duration,
) -> Self {
Self {
uri,
path,
caching: CachingBuilder::MaxAge(max_age),
}
}
}
impl IntoRoute for StaticFiles {
type IntoRoute = StaticFilesRoute;
fn into_route(self) -> StaticFilesRoute {
StaticFilesRoute {
uri: self.uri.into(),
path: self.path.into(),
caching: self.caching.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticFilesOwned {
uri: String,
path: String,
caching: CachingBuilder,
}
impl StaticFilesOwned {
pub fn new(uri: String, path: String) -> Self {
Self {
uri,
path,
caching: CachingBuilder::Default,
}
}
pub fn no_cache(uri: String, path: String) -> Self {
Self {
uri,
path,
caching: CachingBuilder::None,
}
}
pub fn cache_with_age(
uri: String,
path: String,
max_age: Duration,
) -> Self {
Self {
uri,
path,
caching: CachingBuilder::MaxAge(max_age),
}
}
}
impl IntoRoute for StaticFilesOwned {
type IntoRoute = StaticFilesRoute;
fn into_route(self) -> StaticFilesRoute {
StaticFilesRoute {
uri: self.uri.into(),
path: self.path.into(),
caching: self.caching.into(),
}
}
}
#[doc(hidden)]
pub struct StaticFilesRoute {
uri: Cow<'static, str>,
path: Cow<'static, str>,
caching: Option<Caching>,
}
impl Route for StaticFilesRoute {
fn check(&self, header: &RequestHeader) -> bool {
header.method() == &Method::GET
&& header.uri().path().starts_with(&*self.uri)
}
fn validate_data(&self, _data: &Data) {}
fn call<'a>(
&'a self,
req: &'a mut Request,
_: &'a Data,
) -> PinnedFuture<'a, crate::Result<Response>> {
let uri = &self.uri;
let caching = self.caching.clone();
PinnedFuture::new(async move {
let res_path_buf =
req.header().uri().path()[uri.len()..].into_path_buf();
let path_buf = res_path_buf
.map_err(|e| Error::new(ClientErrorKind::BadRequest, e))?;
let path_buf = Path::new(&*self.path).join(path_buf);
tracing::info!("trying to serve {:?}", path_buf);
serve_file(path_buf, &req, caching)
.await
.map_err(Error::from_client_io)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticFile {
uri: &'static str,
path: &'static str,
caching: CachingBuilder,
}
impl StaticFile {
pub const fn new(uri: &'static str, path: &'static str) -> Self {
Self {
uri,
path,
caching: CachingBuilder::Default,
}
}
pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
Self {
uri,
path,
caching: CachingBuilder::None,
}
}
pub const fn cache_with_age(
uri: &'static str,
path: &'static str,
max_age: Duration,
) -> Self {
Self {
uri,
path,
caching: CachingBuilder::MaxAge(max_age),
}
}
}
impl IntoRoute for StaticFile {
type IntoRoute = StaticFileRoute;
fn into_route(self) -> StaticFileRoute {
StaticFileRoute {
uri: self.uri.into(),
path: self.path.into(),
caching: self.caching.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticFileOwned {
uri: String,
path: String,
caching: CachingBuilder,
}
impl StaticFileOwned {
pub const fn new(uri: String, path: String) -> Self {
Self {
uri,
path,
caching: CachingBuilder::Default,
}
}
pub const fn no_cache(uri: String, path: String) -> Self {
Self {
uri,
path,
caching: CachingBuilder::None,
}
}
pub const fn cache_with_age(
uri: String,
path: String,
max_age: Duration,
) -> Self {
Self {
uri,
path,
caching: CachingBuilder::MaxAge(max_age),
}
}
}
impl IntoRoute for StaticFileOwned {
type IntoRoute = StaticFileRoute;
fn into_route(self) -> StaticFileRoute {
StaticFileRoute {
uri: self.uri.into(),
path: self.path.into(),
caching: self.caching.into(),
}
}
}
#[doc(hidden)]
pub struct StaticFileRoute {
uri: Cow<'static, str>,
path: Cow<'static, str>,
caching: Option<Caching>,
}
impl Route for StaticFileRoute {
fn check(&self, header: &RequestHeader) -> bool {
header.method() == &Method::GET
&& check_static(header.uri().path(), &*self.uri)
}
fn validate_data(&self, _data: &Data) {}
fn call<'a>(
&'a self,
req: &'a mut Request,
_: &'a Data,
) -> PinnedFuture<'a, crate::Result<Response>> {
PinnedFuture::new(async move {
serve_file(&*self.path, &req, self.caching.clone())
.await
.map_err(Error::from_client_io)
})
}
}