use std::collections::HashMap; use std::fs; use std::io; use std::path::{Path, PathBuf}; use structopt::StructOpt; use serde::{Serialize, Deserialize}; use tera::{Tera, Filter, Value}; const TMPL_FILES: &'static [&'static str] = &["index.html"]; #[derive(Debug, StructOpt, Serialize)] #[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, help = "Full size url prefix")] pub url_prefix: Option, } #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("reading from file")] Reading { #[from] source: io::Error, }, #[error("parsing config file")] Parsing { #[from] source: serde_yaml::Error, }, #[error("compiling glob pattern")] CompilePattern { #[from] source: glob::PatternError, }, #[error("compiling templates")] CompileTemplate { #[from] source: tera::Error, }, } #[derive(Deserialize, Debug, Serialize)] pub struct Config { pub imageglob: String, pub info: HashMap, pub sizes: 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")) } } } 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) } }