warp_embed/
lib.rs

1//! # warp-embed
2//!
3//! Serve [embedded file](https://crates.io/crates/rust-embed) with [warp](https://crates.io/crates/warp)
4//!
5//! ```ignore
6//! use warp::Filter;
7//! use rust_embed::RustEmbed;
8//!
9//! #[derive(RustEmbed)]
10//! #[folder = "data"]
11//! struct Data;
12//!
13//! let data_serve = warp_embed::embed(&Data);
14//! ```
15
16use std::borrow::Cow;
17use std::sync::Arc;
18use rust_embed::EmbeddedFile;
19use warp::filters::path::{FullPath, Tail};
20use warp::http::Uri;
21use warp::{reject::Rejection, reply::Reply, reply::Response, Filter};
22
23/// Embed serving configuration
24#[derive(Debug, Clone)]
25pub struct EmbedConfig {
26    /// list of directory index.
27    ///
28    /// Default value is `vec!["index.html".to_string(), "index.htm".to_string()]`
29    pub directory_index: Vec<String>,
30}
31
32impl Default for EmbedConfig {
33    fn default() -> Self {
34        EmbedConfig {
35            directory_index: vec!["index.html".to_string(), "index.htm".to_string()],
36        }
37    }
38}
39
40struct EmbedFile {
41    data: Cow<'static, [u8]>,
42}
43
44impl Reply for EmbedFile {
45    fn into_response(self) -> Response {
46        Response::new(self.data.into())
47    }
48}
49
50fn append_filename(path: &str, filename: &str) -> String {
51    if path.is_empty() {
52        filename.to_string()
53    } else {
54        format!("{}/{}", path, filename)
55    }
56}
57
58/// Creates a `Filter` that always serves one embedded file
59pub fn embed_one<A: rust_embed::RustEmbed>(
60    _: &A,
61    filename: &str,
62) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
63    let filename = Arc::new(filename.to_string());
64    warp::any()
65        .map(move || filename.clone())
66        .and_then(|filename: Arc<String>| async move {
67            if let Some(x) = A::get(&filename) {
68                Ok(create_reply(x, &filename))
69            } else {
70                Err(warp::reject::not_found())
71            }
72        })
73}
74
75/// Creates a `Filter` that serves embedded files at the base `path` joined by the request path.
76pub fn embed<A: rust_embed::RustEmbed>(
77    x: &A,
78) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
79    embed_with_config(x, EmbedConfig::default())
80}
81
82#[allow(dead_code)]
83#[derive(Debug)]
84struct NotFound {
85    config: Arc<EmbedConfig>,
86    tail: Tail,
87    full: FullPath,
88}
89
90impl warp::reject::Reject for NotFound {}
91
92/// Creates a `Filter` that serves embedded files at the base `path` joined
93/// by the request path with configuration.
94pub fn embed_with_config<A: rust_embed::RustEmbed>(
95    _: &A,
96    config: EmbedConfig,
97) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
98    let config = Arc::new(config);
99    let config2 = config.clone();
100    let direct_serve = warp::path::tail().and_then(|tail: Tail| async move {
101        if let Some(x) = A::get(tail.as_str()) {
102            Ok(create_reply(x, tail.as_str()))
103        } else {
104            Err(warp::reject::not_found())
105        }
106    });
107
108    let directory_index = warp::any()
109        .map(move || config.clone())
110        .and(warp::path::tail())
111        .and(warp::path::full())
112        .and_then(
113            |config: Arc<EmbedConfig>, tail: Tail, full: FullPath| async move {
114                //eprintln!("directory index: {:?} {:?}", tail, full);
115                for one in config.directory_index.iter() {
116                    if full.as_str().ends_with('/') {
117                        let filepath = format!("{}{}", tail.as_str(), one);
118                        if let Some(x) = A::get(&filepath) {
119                            return Ok(create_reply(x, one));
120                        }
121                    }
122                }
123
124                Err(warp::reject::not_found())
125            },
126        );
127
128    let redirect = warp::any()
129        .map(move || config2.clone())
130        .and(warp::path::tail())
131        .and(warp::path::full())
132        .and_then(
133            |config: Arc<EmbedConfig>, tail: Tail, full: FullPath| async move {
134                for one in config.directory_index.iter() {
135                    if A::get(&append_filename(tail.as_str(), one)).is_some()
136                        && !full.as_str().ends_with('/')
137                    {
138                        let new_path = format!("{}/", full.as_str());
139                        return Ok(warp::redirect(
140                            Uri::builder()
141                                .path_and_query(new_path.as_str())
142                                .build()
143                                .unwrap(),
144                        ));
145                    }
146                }
147
148                Err(warp::reject::not_found())
149            },
150        );
151
152    warp::any()
153        .and(direct_serve)
154        .or(directory_index)
155        .or(redirect)
156}
157
158fn create_reply(file: EmbeddedFile, actual_name: &str) -> impl Reply {
159    let suggest = mime_guess::from_path(actual_name).first_or_octet_stream();
160    warp::reply::with_header(EmbedFile { data: file.data }, "Content-Type", suggest.to_string())
161}
162
163#[cfg(test)]
164mod test;