cache_busters/
lib.rs

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    // Add the StaticFile struct definition to the output
14    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    // Process each asset directory provided
26    for asset_dir in asset_dirs {
27        process_directory(asset_dir, &mut output, &mut static_file_names)?;
28    }
29
30    // Process any extra individual files
31    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    // Write the generated code to the output file
65    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    // Walk through the directory recursively
78    for entry in fs::read_dir(dir)? {
79        let entry = entry?;
80        let path = entry.path();
81
82        if path.is_dir() {
83            // Recursively process subdirectories
84            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    // Get the full path using canonicalize
99    let full_path = fs::canonicalize(&path)?;
100    let file_name = full_path.to_str().unwrap();
101
102    // Generate the hash for cache busting using MD5 and hex encoding
103    let hash = calculate_hash(&path)?;
104
105    // Generate a static-friendly variable name (e.g., assistant_svg)
106    let var_name = path
107        .file_name()
108        .unwrap()
109        .to_str()
110        .unwrap()
111        .replace(['/', '.', '-'], "_");
112
113    // Construct the new hashed filename
114    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    // Generate Rust code for the static file
121    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    // Collect the variable name for the STATICS array
134    static_file_names.push(var_name);
135
136    Ok(())
137}
138
139// Helper function to generate a hash for a file's contents using MD5 and hex encoding
140fn calculate_hash(path: &Path) -> std::io::Result<String> {
141    let mut file = File::open(path)?;
142    let mut buffer = Vec::new();
143
144    // Read the entire file into memory
145    file.read_to_end(&mut buffer)?;
146
147    // Compute the MD5 hash and return it as a hex string
148    let hash = md5::compute(&buffer);
149    Ok(format!("{:x}", hash))
150}
151
152// Helper function to map file extensions to MIME types as strings
153fn 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}