use std::collections::HashMap; use std::fs; use std::io; use std::path::{Path, PathBuf}; use structopt::StructOpt; use serde::Deserialize; use tera::{Tera, Filter, Value}; const TMPL_FILES: &'static [&'static str] = &["index.html"]; #[derive(Debug, StructOpt)] #[structopt(name = "gallery")] pub struct Options { #[structopt(long, help = "Just a thing")] pub check: bool, #[structopt(short, long, help = "Config file location")] pub config: Option, #[structopt(help = "Picture location")] pub load: PathBuf, #[structopt(short, long, default_value = "png", help = "Image extension to use for converted files")] pub ext: String, #[structopt(long, short, default_value = "build", help = "Where to build site")] pub builddir: PathBuf, #[structopt(long, default_value = "1080", help = "Scaled size for image")] pub size_scaled: u32, #[structopt(long, default_value = "720", help = "Thumbnail size for image")] pub size_thumb: u32, } #[derive(Debug)] pub enum ConfigError { Reading(io::Error), Parsing(serde_yaml::Error), CompilePattern(glob::PatternError), CompileTemplate(tera::Error), } #[derive(Deserialize, Debug)] pub struct Config { pub imageglob: String, pub info: HashMap, #[serde(skip)] pub imageglob_compiled: glob::Pattern, } struct ReltoFilter { dir: PathBuf, } #[derive(Debug)] pub struct Context { pub options: Options, pub config: Config, pub imgdir: PathBuf, pub tmpl: Tera, } impl Context { pub fn new_with_args() -> Result { let opts = Options::from_args(); let config = Config::load_with_options(&opts)?; let imgdir = opts.builddir.join("imgs"); // Create img dir if let Err(err) = fs::create_dir(&imgdir) { if err.kind() != io::ErrorKind::AlreadyExists { return Err(ConfigError::from(err)); } } // Compile templates let mut tera = Tera::default(); let tmpl_dir = opts.load.join("templates"); TMPL_FILES.iter().map(|fname| -> Result<(), ConfigError> { let path = tmpl_dir.join(fname); tera.add_template_file(path, Some(fname))?; Ok(()) }).collect::>()?; tera.register_filter("relto_build", ReltoFilter::new(&opts.builddir)); Ok(Context { options: opts, config: config, imgdir: imgdir, tmpl: tera, }) } pub fn get_image_files(&self) -> Result, io::Error> { let files = fs::read_dir(&self.options.load)? // Remove errored entries (SILENTLY) .filter(|entry| entry.is_ok()) .map(|entry| entry.unwrap().path()) // Filter out directories and files that do not match pattern .filter(|path| path.file_name() .map_or(false, |file| self.config.imageglob_compiled.matches(file.to_str().unwrap())) ) .collect(); Ok(files) } } impl ReltoFilter { pub fn new(dir: &Path) -> Self { ReltoFilter { dir: dir.to_path_buf(), } } } impl Filter for ReltoFilter { fn filter(&self, value: &Value, _: &HashMap) -> tera::Result { if let Value::String(path) = value { let path = PathBuf::from(path); match path.strip_prefix(&self.dir) { Ok(relto) => Ok(Value::String(relto.to_string_lossy().to_string())), Err(err) => Err(tera::Error::call_filter("relto", err)), } } else { Err(tera::Error::msg("Input to relto filter must be string")) } } fn is_safe(&self) -> bool { true } } impl Config { fn load_from_file(path: &PathBuf) -> Result { let content = fs::read_to_string(path)?; let mut config: Config = serde_yaml::from_str(&content)?; config.imageglob_compiled = glob::Pattern::new(&config.imageglob)?; Ok(config) } pub fn load_with_options(opts: &Options) -> Result { let config_path = match &opts.config { Some(path) => path.to_path_buf(), None => opts.load.join("imginfo.yml"), }; Config::load_from_file(&config_path) } } impl From for ConfigError { fn from(error: io::Error) -> Self { ConfigError::Reading(error) } } impl From for ConfigError { fn from(error: serde_yaml::Error) -> Self { ConfigError::Parsing(error) } } impl From for ConfigError { fn from(error: glob::PatternError) -> Self { ConfigError::CompilePattern(error) } } impl From for ConfigError { fn from(error: tera::Error) -> Self { ConfigError::CompileTemplate(error) } }