static_files/mods/
resource.rs

1/*!
2Resource definition and single module based generation.
3 */
4use std::{
5    fs::{self, File, Metadata},
6    io::{self, Write},
7    path::{Path, PathBuf},
8    time::SystemTime,
9};
10
11use path_slash::PathExt;
12
13/// Static files resource.
14pub struct Resource {
15    pub data: &'static [u8],
16    pub modified: u64,
17    pub mime_type: &'static str,
18}
19
20/// Used internally in generated functions.
21#[inline]
22#[must_use]
23pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource {
24    Resource {
25        data,
26        modified,
27        mime_type,
28    }
29}
30
31pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r";
32
33/// Generate resources for `project_dir` using `filter`.
34/// Result saved in `generated_filename` and function named as `fn_name`.
35///
36/// in `build.rs`:
37/// ```rust
38/// use std::{env, path::Path};
39/// use static_files::resource::generate_resources;
40///
41/// fn main() {
42///     let out_dir = env::var("OUT_DIR").unwrap();
43///     let generated_filename = Path::new(&out_dir).join("generated.rs");
44///     generate_resources("./tests", None, generated_filename, "generate").unwrap();
45/// }
46/// ```
47///
48/// in `main.rs`:
49/// ```rust
50/// include!(concat!(env!("OUT_DIR"), "/generated.rs"));
51///
52/// fn main() {
53///     let generated_file = generate();
54///
55///     assert_eq!(generated_file.len(), 4);
56/// }
57/// ```
58pub fn generate_resources<P: AsRef<Path>, G: AsRef<Path>>(
59    project_dir: P,
60    filter: Option<fn(p: &Path) -> bool>,
61    generated_filename: G,
62    fn_name: &str,
63) -> io::Result<()> {
64    let resources = collect_resources(&project_dir, filter)?;
65
66    let mut f = File::create(&generated_filename)?;
67
68    generate_function_header(&mut f, fn_name)?;
69    generate_uses(&mut f)?;
70
71    generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
72    generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, &resources)?;
73    generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
74
75    generate_function_end(&mut f)?;
76
77    Ok(())
78}
79
80/// Generate resource mapping for `project_dir` using `filter`.
81/// Result saved in `generated_filename` as anonymous block which returns `HashMap<&'static str, Resource>`.
82///
83/// in `build.rs`:
84/// ```rust
85///
86/// use std::{env, path::Path};
87/// use static_files::resource::generate_resources_mapping;
88///
89/// fn main() {
90///     let out_dir = env::var("OUT_DIR").unwrap();
91///     let generated_filename = Path::new(&out_dir).join("generated_mapping.rs");
92///     generate_resources_mapping("./tests", None, generated_filename).unwrap();
93/// }
94/// ```
95///
96/// in `main.rs`:
97/// ```rust
98/// use std::collections::HashMap;
99///
100/// use static_files::Resource;
101///
102/// fn generate_mapping() -> HashMap<&'static str, Resource> {
103///   include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs"))
104/// }
105///
106/// fn main() {
107///     let generated_file = generate_mapping();
108///
109///     assert_eq!(generated_file.len(), 4);
110///
111/// }
112/// ```
113pub fn generate_resources_mapping<P: AsRef<Path>, G: AsRef<Path>>(
114    project_dir: P,
115    filter: Option<fn(p: &Path) -> bool>,
116    generated_filename: G,
117) -> io::Result<()> {
118    let resources = collect_resources(&project_dir, filter)?;
119
120    let mut f = File::create(&generated_filename)?;
121    writeln!(f, "{{")?;
122
123    generate_uses(&mut f)?;
124
125    generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
126
127    generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, &resources)?;
128
129    generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
130
131    writeln!(f, "}}")?;
132    Ok(())
133}
134
135#[cfg(not(feature = "sort"))]
136pub(crate) fn collect_resources<P: AsRef<Path>>(
137    path: P,
138    filter: Option<fn(p: &Path) -> bool>,
139) -> io::Result<Vec<(PathBuf, Metadata)>> {
140    collect_resources_nested(path, filter)
141}
142
143#[cfg(feature = "sort")]
144pub(crate) fn collect_resources<P: AsRef<Path>>(
145    path: P,
146    filter: Option<fn(p: &Path) -> bool>,
147) -> io::Result<Vec<(PathBuf, Metadata)>> {
148    let mut resources = collect_resources_nested(path, filter)?;
149    resources.sort_by(|a, b| a.0.cmp(&b.0));
150    Ok(resources)
151}
152
153#[inline]
154fn collect_resources_nested<P: AsRef<Path>>(
155    path: P,
156    filter: Option<fn(p: &Path) -> bool>,
157) -> io::Result<Vec<(PathBuf, Metadata)>> {
158    let mut result = vec![];
159
160    for entry in fs::read_dir(&path)? {
161        let entry = entry?;
162        let path = entry.path();
163
164        if let Some(ref filter) = filter {
165            if !filter(path.as_ref()) {
166                continue;
167            }
168        }
169
170        if path.is_dir() {
171            let nested = collect_resources(path, filter)?;
172            result.extend(nested);
173        } else {
174            result.push((path, entry.metadata()?));
175        }
176    }
177
178    Ok(result)
179}
180
181pub(crate) fn generate_resource_inserts<P: AsRef<Path>, W: Write>(
182    f: &mut W,
183    project_dir: &P,
184    variable_name: &str,
185    resources: &[(PathBuf, Metadata)],
186) -> io::Result<()> {
187    for resource in resources {
188        generate_resource_insert(f, project_dir, variable_name, resource)?;
189    }
190    Ok(())
191}
192
193pub(crate) fn generate_resource_insert<P: AsRef<Path>, W: Write>(
194    f: &mut W,
195    project_dir: &P,
196    variable_name: &str,
197    resource: &(PathBuf, Metadata),
198) -> io::Result<()> {
199    let (path, metadata) = resource;
200    let abs_path = path.canonicalize()?;
201    let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap();
202
203    let modified = if let Ok(Ok(modified)) = metadata
204        .modified()
205        .map(|x| x.duration_since(SystemTime::UNIX_EPOCH))
206    {
207        modified.as_secs()
208    } else {
209        0
210    };
211    let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
212    writeln!(
213        f,
214        "{variable_name}.insert({key_path:?},n(i!({abs_path:?}),{modified:?},{mime_type:?}));",
215    )
216}
217
218pub(crate) fn generate_function_header<F: Write>(f: &mut F, fn_name: &str) -> io::Result<()> {
219    writeln!(
220        f,
221        "#[allow(clippy::unreadable_literal)] #[must_use] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::static_files::Resource> {{",
222    )
223}
224
225pub(crate) fn generate_function_end<F: Write>(f: &mut F) -> io::Result<()> {
226    writeln!(f, "}}")
227}
228
229pub(crate) fn generate_uses<F: Write>(f: &mut F) -> io::Result<()> {
230    writeln!(
231        f,
232        "\
233use ::static_files::resource::new_resource as n;
234use ::std::include_bytes as i;",
235    )
236}
237
238pub(crate) fn generate_variable_header<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
239    writeln!(
240        f,
241        "let mut {variable_name} = ::std::collections::HashMap::new();",
242    )
243}
244
245pub(crate) fn generate_variable_return<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
246    writeln!(f, "{variable_name}")
247}