Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support configurable backend for ServeDir #313

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test-files/subdir/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hi from foo.txt
1 change: 1 addition & 0 deletions tower-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ tower = { version = "0.4.10", features = ["buffer", "util", "retry", "make", "ti
tracing-subscriber = "0.3"
uuid = { version = "1.0", features = ["v4"] }
serde_json = "1.0"
rust-embed = "6.4"

[features]
default = []
Expand Down
98 changes: 98 additions & 0 deletions tower-http/src/services/fs/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use futures_util::future::BoxFuture;
use std::{future::Future, io, path::Path, time::SystemTime};
use tokio::io::{AsyncRead, AsyncSeek};

// TODO(david): try and rewrite this using async-trait to see if that matters much
// currently this requires GATs which is maybe pushing tower-http's MSRV a bit?
//
// async-trait is unfortunate because it requires syn+quote and requiring allocations
// futures is unfortunate if the data is in the binary, as for rust-embed

// TODO(david): implement a backend using rust-embed to prove that its possible

pub trait Backend: Clone + Send + Sync + 'static {
type File: File<Metadata = Self::Metadata>;
type Metadata: Metadata;

type OpenFuture: Future<Output = io::Result<Self::File>> + Send;
type MetadataFuture: Future<Output = io::Result<Self::Metadata>> + Send;

fn open<A>(&self, path: A) -> Self::OpenFuture
where
A: AsRef<Path>;

fn metadata<A>(&self, path: A) -> Self::MetadataFuture
where
A: AsRef<Path>;
}

pub trait Metadata: Send + 'static {
fn is_dir(&self) -> bool;

fn modified(&self) -> io::Result<SystemTime>;

fn len(&self) -> u64;
}

pub trait File: AsyncRead + AsyncSeek + Unpin + Send + Sync {
type Metadata: Metadata;
type MetadataFuture<'a>: Future<Output = io::Result<Self::Metadata>> + Send
where
Self: 'a;

fn metadata(&self) -> Self::MetadataFuture<'_>;
}

#[derive(Default, Debug, Clone)]
#[non_exhaustive]
pub struct TokioBackend;

impl Backend for TokioBackend {
type File = tokio::fs::File;
type Metadata = std::fs::Metadata;

type OpenFuture = BoxFuture<'static, io::Result<Self::File>>;
type MetadataFuture = BoxFuture<'static, io::Result<Self::Metadata>>;

fn open<A>(&self, path: A) -> Self::OpenFuture
where
A: AsRef<Path>,
{
let path = path.as_ref().to_owned();
Box::pin(tokio::fs::File::open(path))
}

fn metadata<A>(&self, path: A) -> Self::MetadataFuture
where
A: AsRef<Path>,
{
let path = path.as_ref().to_owned();
Box::pin(tokio::fs::metadata(path))
}
}

impl File for tokio::fs::File {
type Metadata = std::fs::Metadata;
type MetadataFuture<'a> = BoxFuture<'a, io::Result<Self::Metadata>>;

fn metadata(&self) -> Self::MetadataFuture<'_> {
Box::pin(self.metadata())
}
}

impl Metadata for std::fs::Metadata {
#[inline]
fn is_dir(&self) -> bool {
self.is_dir()
}

#[inline]
fn modified(&self) -> io::Result<SystemTime> {
self.modified()
}

#[inline]
fn len(&self) -> u64 {
self.len()
}
}
2 changes: 2 additions & 0 deletions tower-http/src/services/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ use std::{
use tokio::io::{AsyncRead, AsyncReadExt, Take};
use tokio_util::io::ReaderStream;

mod backend;
mod serve_dir;
mod serve_file;

pub use self::{
backend::{Backend, File, Metadata},
serve_dir::{
future::ResponseFuture as ServeFileSystemResponseFuture,
DefaultServeDirFallback,
Expand Down
36 changes: 22 additions & 14 deletions tower-http/src/services/fs/serve_dir/future.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use super::{
open_file::{FileOpened, FileRequestExtent, OpenFileOutput},
DefaultServeDirFallback, ResponseBody,
ResponseBody,
};
use crate::{
services::fs::{AsyncReadBody, Backend, Metadata as _},
BoxError,
};
use crate::{services::fs::AsyncReadBody, BoxError};
use bytes::Bytes;
use futures_util::{
future::{BoxFuture, FutureExt, TryFutureExt},
Expand All @@ -25,15 +28,15 @@ use tower_service::Service;

pin_project! {
/// Response future of [`ServeDir::try_call`].
pub struct ResponseFuture<ReqBody, F = DefaultServeDirFallback> {
pub struct ResponseFuture<ReqBody, F, B: Backend> {
#[pin]
pub(super) inner: ResponseFutureInner<ReqBody, F>,
pub(super) inner: ResponseFutureInner<ReqBody, F, B>,
}
}

impl<ReqBody, F> ResponseFuture<ReqBody, F> {
impl<ReqBody, F, B: Backend> ResponseFuture<ReqBody, F, B> {
pub(super) fn open_file_future(
future: BoxFuture<'static, io::Result<OpenFileOutput>>,
future: BoxFuture<'static, io::Result<OpenFileOutput<B>>>,
fallback_and_request: Option<(F, Request<ReqBody>)>,
) -> Self {
Self {
Expand Down Expand Up @@ -61,10 +64,10 @@ impl<ReqBody, F> ResponseFuture<ReqBody, F> {

pin_project! {
#[project = ResponseFutureInnerProj]
pub(super) enum ResponseFutureInner<ReqBody, F> {
pub(super) enum ResponseFutureInner<ReqBody, F, B: Backend> {
OpenFileFuture {
#[pin]
future: BoxFuture<'static, io::Result<OpenFileOutput>>,
future: BoxFuture<'static, io::Result<OpenFileOutput<B>>>,
fallback_and_request: Option<(F, Request<ReqBody>)>,
},
FallbackFuture {
Expand All @@ -77,12 +80,13 @@ pin_project! {
}
}

impl<F, ReqBody, ResBody> Future for ResponseFuture<ReqBody, F>
impl<F, B, ReqBody, ResBody> Future for ResponseFuture<ReqBody, F, B>
where
F: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible> + Clone,
F::Future: Send + 'static,
ResBody: http_body::Body<Data = Bytes> + Send + 'static,
ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
B: Backend,
{
type Output = io::Result<Response<ResponseBody>>;

Expand Down Expand Up @@ -176,15 +180,16 @@ fn not_found() -> Response<ResponseBody> {
response_with_status(StatusCode::NOT_FOUND)
}

pub(super) fn call_fallback<F, B, FResBody>(
pub(super) fn call_fallback<F, ReqBody, FResBody, B>(
fallback: &mut F,
req: Request<B>,
) -> ResponseFutureInner<B, F>
req: Request<ReqBody>,
) -> ResponseFutureInner<ReqBody, F, B>
where
F: Service<Request<B>, Response = Response<FResBody>, Error = Infallible> + Clone,
F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
F::Future: Send + 'static,
FResBody: http_body::Body<Data = Bytes> + Send + 'static,
FResBody::Error: Into<BoxError>,
B: Backend,
{
let future = fallback
.call(req)
Expand All @@ -204,7 +209,10 @@ where
ResponseFutureInner::FallbackFuture { future }
}

fn build_response(output: FileOpened) -> Response<ResponseBody> {
fn build_response<B>(output: FileOpened<B>) -> Response<ResponseBody>
where
B: Backend,
{
let (maybe_file, size) = match output.extent {
FileRequestExtent::Full(file, meta) => (Some(file), meta.len()),
FileRequestExtent::Head(meta) => (None, meta.len()),
Expand Down
Loading