xitca_web/service/
file.rs

1//! static file serving.
2
3use core::convert::Infallible;
4
5use std::path::PathBuf;
6
7use http_file::{ServeDir as _ServeDir, runtime::AsyncFs};
8use xitca_http::util::service::router::{PathGen, RouteGen};
9
10use crate::service::Service;
11
12/// builder type for serve dir service.
13pub struct ServeDir<F: AsyncFs = dumb::Dumb> {
14    inner: _ServeDir<F>,
15}
16
17#[cfg(feature = "file")]
18impl ServeDir {
19    /// construct a new static file service that serve given relative file path's file based on file name.
20    ///
21    /// # Example
22    /// ```rust
23    /// # use xitca_web::{
24    /// #     handler::{handler_service},
25    /// #     service::file::ServeDir,
26    /// #     App, WebContext
27    /// # };
28    /// App::new()
29    ///     // http request would be matched against files inside ./static file path.
30    ///     .at("/", ServeDir::new("static"))
31    ///     // other named routes have higher priority than serve dir service.
32    ///     .at("/foo", handler_service(|| async { "foo!" }))
33    ///     # .at("/bar", handler_service(|_: &WebContext<'_>| async { "used for inferring types!" }));
34    /// ```
35    pub fn new(path: impl Into<PathBuf>) -> ServeDir<impl AsyncFs + Clone> {
36        ServeDir {
37            inner: _ServeDir::new(path),
38        }
39    }
40
41    #[cfg(feature = "io-uring")]
42    pub fn new_tokio_uring(path: impl Into<PathBuf>) -> ServeDir<impl AsyncFs + Clone> {
43        ServeDir {
44            inner: _ServeDir::new_tokio_uring(path),
45        }
46    }
47}
48
49impl<F> ServeDir<F>
50where
51    F: AsyncFs,
52{
53    /// construct a new static file service with given file system. file system must be a type impl [AsyncFs]
54    /// trait to instruct how async read/write of disk(or in memory) file can be performed.
55    pub fn with_fs(path: impl Into<PathBuf>, fs: F) -> Self {
56        ServeDir {
57            inner: _ServeDir::with_fs(path, fs),
58        }
59    }
60}
61
62impl<F> PathGen for ServeDir<F>
63where
64    F: AsyncFs,
65{
66    fn path_gen(&mut self, prefix: &str) -> String {
67        let mut prefix = String::from(prefix);
68        if prefix.ends_with('/') {
69            prefix.pop();
70        }
71
72        prefix.push_str("/*p");
73
74        prefix
75    }
76}
77
78impl<F> RouteGen for ServeDir<F>
79where
80    F: AsyncFs,
81{
82    type Route<R> = R;
83
84    fn route_gen<R>(route: R) -> Self::Route<R> {
85        route
86    }
87}
88
89impl<F> Service for ServeDir<F>
90where
91    F: AsyncFs + Clone,
92{
93    type Response = service::ServeDirService<F>;
94    type Error = Infallible;
95
96    async fn call(&self, _: ()) -> Result<Self::Response, Self::Error> {
97        Ok(service::ServeDirService(self.inner.clone()))
98    }
99}
100
101mod service {
102    use http_file::{ServeDir, ServeError, runtime::AsyncFs};
103
104    use crate::{
105        body::ResponseBody,
106        context::WebContext,
107        error::{Error, ErrorStatus, MatchError, MethodNotAllowed, RouterError},
108        http::{Method, StatusCode, WebResponse},
109        service::Service,
110    };
111
112    pub struct ServeDirService<F: AsyncFs>(pub(super) ServeDir<F>);
113
114    impl<'r, C, B, F> Service<WebContext<'r, C, B>> for ServeDirService<F>
115    where
116        F: AsyncFs,
117        F::File: 'static,
118    {
119        type Response = WebResponse;
120        type Error = RouterError<Error>;
121
122        async fn call(&self, ctx: WebContext<'r, C, B>) -> Result<Self::Response, Self::Error> {
123            match self.0.serve(ctx.req()).await {
124                Ok(res) => Ok(res.map(ResponseBody::box_stream)),
125                Err(ServeError::NotModified) => {
126                    let mut res = ctx.into_response(ResponseBody::none());
127                    *res.status_mut() = StatusCode::NOT_MODIFIED;
128                    Ok(res)
129                }
130                Err(e) => Err(match e {
131                    ServeError::NotFound => RouterError::Match(MatchError),
132                    ServeError::MethodNotAllowed => {
133                        RouterError::NotAllowed(MethodNotAllowed(Box::new(vec![Method::GET, Method::HEAD])))
134                    }
135                    ServeError::Io(io) => RouterError::Service(Error::from(io)),
136                    _ => RouterError::Service(Error::from(ErrorStatus::bad_request())),
137                }),
138            }
139        }
140    }
141}
142
143mod dumb {
144    use core::future::Ready;
145
146    use std::{io, path::PathBuf};
147
148    use http_file::runtime::{AsyncFs, ChunkRead, Meta};
149
150    use crate::bytes::BytesMut;
151
152    // a dummy type to help make ServerDir::new work as is.
153    #[derive(Clone, Copy)]
154    pub struct Dumb;
155
156    impl AsyncFs for Dumb {
157        type File = DumbFile;
158
159        type OpenFuture = Ready<io::Result<Self::File>>;
160
161        fn open(&self, _: PathBuf) -> Self::OpenFuture {
162            unimplemented!()
163        }
164    }
165
166    // just like Dumb
167    pub struct DumbFile;
168
169    impl Meta for DumbFile {
170        fn modified(&mut self) -> Option<std::time::SystemTime> {
171            unimplemented!()
172        }
173
174        fn len(&self) -> u64 {
175            unimplemented!()
176        }
177    }
178
179    impl ChunkRead for DumbFile {
180        type SeekFuture<'f>
181            = Ready<io::Result<()>>
182        where
183            Self: 'f;
184        type Future = Ready<io::Result<Option<(Self, BytesMut, usize)>>>;
185
186        fn seek(&mut self, _: io::SeekFrom) -> Self::SeekFuture<'_> {
187            unimplemented!()
188        }
189
190        fn next(self, _: xitca_http::bytes::BytesMut) -> Self::Future {
191            unimplemented!()
192        }
193    }
194}