use serde::Deserialize; use std::{fs, path::PathBuf}; use glob::glob; use std::{fs::File, io::Write, path::Path}; use crate::template::Template; /// Store the configuration of config/config.toml #[derive(Clone, Debug, Default, Deserialize)] pub struct FileConfiguration { /// http/https pub scheme: Option, /// Domain name "sub.domain.tld" pub domain: Option, /// Port used pub port: Option, /// Mail of owner pub mail: Option, /// Lang used pub lang: Option, /// .onion address for Tor of the app pub onion: Option, /// App name pub app_name: Option, /// Name of website owner pub name: Option, /// Fullname of website owner pub fullname: Option, /// List exclusion for courses pub exclude_courses: Option>, } impl FileConfiguration { /// Initialize with default values fn new() -> Self { Self { scheme: Some("http".into()), domain: Some("localhost".into()), port: Some(8080), app_name: Some("EWP".into()), exclude_courses: Some([].into()), ..Self::default() } } /// Complete default structure with an existing one fn complete(a: Self) -> Self { // Default config let d = Self::new(); #[allow(clippy::items_after_statements)] /// Return the default value if nothing is value is none fn test(val: Option, default: Option) -> Option { if val.is_some() { val } else { default } } Self { scheme: test(a.scheme, d.scheme), port: test(a.port, d.port), mail: test(a.mail, d.mail), lang: test(a.lang, d.lang), domain: test(a.domain, d.domain), onion: test(a.onion, d.onion), app_name: test(a.app_name, d.app_name), name: test(a.name, d.name), fullname: test(a.fullname, d.fullname), exclude_courses: test(a.exclude_courses, d.exclude_courses), } } } // Paths where files are stored #[derive(Clone, Debug)] pub struct Locations { pub static_dir: String, pub data_dir: String, } /// Configuration used internally in the app #[derive(Clone, Debug)] pub struct Config { /// Information given in the config file pub fc: FileConfiguration, /// Location where the static files are stored pub locations: Locations, /// Informations about templates pub tmpl: Template, } /// Load the config file fn get_file_config(file_path: &str) -> FileConfiguration { fs::read_to_string(file_path).map_or_else( |_| FileConfiguration::new(), |file| match toml::from_str(&file) { Ok(stored_config) => FileConfiguration::complete(stored_config), Err(file_error) => { panic!("Error in config file: {file_error}"); } }, ) } /// Build the configuration pub fn get_configuration(file_path: &str) -> Config { let internal_config = get_file_config(file_path); let static_dir = "static"; let templates_dir = "templates"; let files_root = init("dist".into(), static_dir, templates_dir); Config { fc: internal_config.clone(), locations: Locations { static_dir: format!("{files_root}/{static_dir}"), data_dir: String::from("data"), }, tmpl: Template { directory: format!("{files_root}/{templates_dir}"), app_name: internal_config.app_name.unwrap(), url: internal_config.domain.unwrap(), name: internal_config.name, }, } } /// Preparation before running the http server fn init(dist_dir: String, static_dir: &str, templates_dir: &str) -> String { // The static folder is minimized only in release mode if cfg!(debug_assertions) { ".".into() } else { let cfg = minify_html::Cfg { keep_closing_tags: true, preserve_brace_template_syntax: true, minify_css: true, minify_js: true, ..minify_html::Cfg::spec_compliant() }; // Static files for entry in glob(&format!("{static_dir}/**/*.*")).unwrap() { let path = entry.unwrap(); let path_with_dist = path .to_string_lossy() .replace(static_dir, &format!("{dist_dir}/{static_dir}")); minify_and_copy(&cfg, path, path_with_dist); } // Template files for entry in glob(&format!("{templates_dir}/**/*.*")).unwrap() { let path = entry.unwrap(); let path_with_dist = path .to_string_lossy() .replace(templates_dir, &format!("{dist_dir}/{templates_dir}")); minify_and_copy(&cfg, path, path_with_dist); } dist_dir } } /// Minify some assets for production fn minify_and_copy(cfg: &minify_html::Cfg, path: PathBuf, path_with_dist: String) { // Create folders let new_path = Path::new(&path_with_dist); fs::create_dir_all(new_path.parent().unwrap()).unwrap(); let session = minify_js::Session::new(); let mut copy = true; if let Some(ext) = path.extension() { let js_ext = "js"; let current_ext = ext.to_string_lossy().to_lowercase(); // List of files who should be minified if ["html", "css", js_ext, "svg", "webmanifest", "xml"].contains(¤t_ext.as_str()) { // We won't copy, we'll minify copy = false; // Minify let data = fs::read(&path).unwrap(); let minified = if current_ext == js_ext { let mut out = Vec::new(); minify_js::minify(&session, minify_js::TopLevelMode::Global, &data, &mut out) .unwrap(); out } else { minify_html::minify(&data, cfg) }; // Write files let file = File::create(&path_with_dist); file.expect("Error when minify the file") .write_all(&minified) .unwrap(); } } if copy { // If no minification is needed fs::copy(path, path_with_dist).unwrap(); } }