1use std::fs::{self, File};
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4
5pub fn generate_static_files_code(
6 out_dir: &Path,
7 asset_dirs: &[PathBuf],
8 extra_files: &[PathBuf],
9) -> std::io::Result<()> {
10 let mut output = String::new();
11 let mut static_file_names = Vec::new();
12
13 output.push_str(
15 r#"
16 pub mod statics {
17 pub struct StaticFile {
18 pub file_name: &'static str,
19 pub name: &'static str,
20 pub mime: &'static str,
21 }
22 "#,
23 );
24
25 for asset_dir in asset_dirs {
27 process_directory(asset_dir, &mut output, &mut static_file_names)?;
28 }
29
30 for file_path in extra_files {
32 process_file(file_path, &mut output, &mut static_file_names)?;
33 }
34
35 output.push_str(
36 r#"#[allow(dead_code)]
37 impl StaticFile {
38 /// Get a single `StaticFile` by name, if it exists.
39 #[must_use]
40 pub fn get(name: &str) -> Option<&'static Self> {
41 if let Some(pos) = STATICS.iter().position(|&s| name == s.name) {
42 Some(STATICS[pos])
43 } else {
44 None
45 }
46 }
47 }
48 "#,
49 );
50
51 let statics_array = static_file_names
52 .iter()
53 .map(|name| format!("&{}", name))
54 .collect::<Vec<_>>()
55 .join(", ");
56
57 output.push_str(&format!(
58 "pub static STATICS: &[&StaticFile] = &[{}];",
59 statics_array
60 ));
61
62 output.push('}');
63
64 let out_file_path = out_dir.join("static_files.rs");
66 let mut out_file = File::create(out_file_path)?;
67 out_file.write_all(output.as_bytes())?;
68
69 Ok(())
70}
71
72fn process_directory(
73 dir: &Path,
74 output: &mut String,
75 static_file_names: &mut Vec<String>,
76) -> std::io::Result<()> {
77 for entry in fs::read_dir(dir)? {
79 let entry = entry?;
80 let path = entry.path();
81
82 if path.is_dir() {
83 process_directory(&path, output, static_file_names)?;
85 } else if path.is_file() {
86 process_file(&path, output, static_file_names)?;
87 }
88 }
89
90 Ok(())
91}
92
93fn process_file(
94 path: &Path,
95 output: &mut String,
96 static_file_names: &mut Vec<String>,
97) -> std::io::Result<()> {
98 let full_path = fs::canonicalize(&path)?;
100 let file_name = full_path.to_str().unwrap();
101
102 let hash = calculate_hash(&path)?;
104
105 let var_name = path
107 .file_name()
108 .unwrap()
109 .to_str()
110 .unwrap()
111 .replace(['/', '.', '-'], "_");
112
113 let file_stem = path.file_stem().unwrap().to_str().unwrap();
115 let extension = path.extension().unwrap().to_str().unwrap();
116 let hashed_name = format!("{file_stem}-{hash}.{extension}");
117
118 let mime_type = mime_type_from_extension(extension);
119
120 output.push_str(&format!(
122 r#"
123 /// From "{file_name}"
124 #[allow(non_upper_case_globals)]
125 pub static {var_name}: StaticFile = StaticFile {{
126 file_name: "{file_name}",
127 name: "/static/{hashed_name}",
128 mime: "{mime_type}",
129 }};
130 "#,
131 ));
132
133 static_file_names.push(var_name);
135
136 Ok(())
137}
138
139fn calculate_hash(path: &Path) -> std::io::Result<String> {
141 let mut file = File::open(path)?;
142 let mut buffer = Vec::new();
143
144 file.read_to_end(&mut buffer)?;
146
147 let hash = md5::compute(&buffer);
149 Ok(format!("{:x}", hash))
150}
151
152fn mime_type_from_extension(extension: &str) -> &'static str {
154 match extension {
155 "svg" => "image/svg+xml",
156 "png" => "image/png",
157 "jpg" | "jpeg" => "image/jpeg",
158 "css" => "text/css",
159 "js" => "application/javascript",
160 "wasm" => "application/wasm",
161 _ => "application/octet-stream",
162 }
163}