use path_slash::PathExt; use std::{ fs::{self, File, Metadata}, io::{self, Write}, path::{Path, PathBuf}, time::SystemTime, }; /// Static files resource. pub struct Resource { pub data: &'static [u8], pub modified: u64, pub mime_type: &'static str, } /// Used internally in generated functions. #[inline] pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource { Resource { data, modified, mime_type, } } pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r"; /// Generate resources for `project_dir` using `filter`. /// Result saved in `generated_filename` and function named as `fn_name`. /// /// in `build.rs`: /// ```rust /// use std::{env, path::Path}; /// use pagetop_statics::resource::generate_resources; /// /// fn main() { /// let out_dir = env::var("OUT_DIR").unwrap(); /// let generated_filename = Path::new(&out_dir).join("generated.rs"); /// generate_resources("./tests", None, generated_filename, "generate", "pagetop_statics").unwrap(); /// } /// ``` /// /// in `main.rs`: /// ```rust /// include!(concat!(env!("OUT_DIR"), "/generated.rs")); /// /// fn main() { /// let generated_file = generate(); /// /// assert_eq!(generated_file.len(), 4); /// } /// ``` pub fn generate_resources, G: AsRef>( project_dir: P, filter: Option bool>, generated_filename: G, fn_name: &str, crate_name: &str, ) -> io::Result<()> { let resources = collect_resources(&project_dir, filter)?; let mut f = File::create(&generated_filename)?; generate_function_header(&mut f, fn_name, crate_name)?; generate_uses(&mut f, crate_name)?; generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?; generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?; generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?; generate_function_end(&mut f)?; Ok(()) } /// Generate resource mapping for `project_dir` using `filter`. /// Result saved in `generated_filename` as anonymous block which returns HashMap<&'static str, Resource>. /// /// in `build.rs`: /// ```rust /// /// use std::{env, path::Path}; /// use pagetop_statics::resource::generate_resources_mapping; /// /// fn main() { /// let out_dir = env::var("OUT_DIR").unwrap(); /// let generated_filename = Path::new(&out_dir).join("generated_mapping.rs"); /// generate_resources_mapping("./tests", None, generated_filename, "pagetop_statics").unwrap(); /// } /// ``` /// /// in `main.rs`: /// ```rust /// use std::collections::HashMap; /// /// use pagetop_statics::StaticResource; /// /// fn generate_mapping() -> HashMap<&'static str, StaticResource> { /// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs")) /// } /// /// fn main() { /// let generated_file = generate_mapping(); /// /// assert_eq!(generated_file.len(), 4); /// /// } /// ``` pub fn generate_resources_mapping, G: AsRef>( project_dir: P, filter: Option bool>, generated_filename: G, crate_name: &str, ) -> io::Result<()> { let resources = collect_resources(&project_dir, filter)?; let mut f = File::create(&generated_filename)?; writeln!(f, "{{")?; generate_uses(&mut f, crate_name)?; generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?; generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?; generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?; writeln!(f, "}}")?; Ok(()) } #[cfg(not(feature = "sort"))] pub(crate) fn collect_resources>( path: P, filter: Option bool>, ) -> io::Result> { collect_resources_nested(path, filter) } #[cfg(feature = "sort")] pub(crate) fn collect_resources>( path: P, filter: Option bool>, ) -> io::Result> { let mut resources = collect_resources_nested(path, filter)?; resources.sort_by(|a, b| a.0.cmp(&b.0)); Ok(resources) } #[inline] fn collect_resources_nested>( path: P, filter: Option bool>, ) -> io::Result> { let mut result = vec![]; for entry in fs::read_dir(&path)? { let entry = entry?; let path = entry.path(); if let Some(ref filter) = filter { if !filter(path.as_ref()) { continue; } } if path.is_dir() { let nested = collect_resources(path, filter)?; result.extend(nested); } else { result.push((path, entry.metadata()?)); } } Ok(result) } pub(crate) fn generate_resource_inserts, W: Write>( f: &mut W, project_dir: &P, variable_name: &str, resources: Vec<(PathBuf, Metadata)>, ) -> io::Result<()> { for resource in &resources { generate_resource_insert(f, project_dir, variable_name, resource)?; } Ok(()) } #[allow(clippy::unnecessary_debug_formatting)] pub(crate) fn generate_resource_insert, W: Write>( f: &mut W, project_dir: &P, variable_name: &str, resource: &(PathBuf, Metadata), ) -> io::Result<()> { let (path, metadata) = resource; let abs_path = path.canonicalize()?; let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap(); let modified = if let Ok(Ok(modified)) = metadata .modified() .map(|x| x.duration_since(SystemTime::UNIX_EPOCH)) { modified.as_secs() } else { 0 }; let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream(); writeln!( f, "{}.insert({:?},n(i!({:?}),{:?},{:?}));", variable_name, &key_path, &abs_path, modified, &mime_type, ) } pub(crate) fn generate_function_header( f: &mut F, fn_name: &str, crate_name: &str, ) -> io::Result<()> { writeln!( f, "#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{", ) } pub(crate) fn generate_function_end(f: &mut F) -> io::Result<()> { writeln!(f, "}}") } pub(crate) fn generate_uses(f: &mut F, crate_name: &str) -> io::Result<()> { writeln!( f, "use ::{crate_name}::resource::new_resource as n; use ::std::include_bytes as i;", ) } pub(crate) fn generate_variable_header(f: &mut F, variable_name: &str) -> io::Result<()> { writeln!( f, "let mut {variable_name} = ::std::collections::HashMap::new();", ) } pub(crate) fn generate_variable_return(f: &mut F, variable_name: &str) -> io::Result<()> { writeln!(f, "{variable_name}") }