// #[global_allocator] // static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; #[macro_use] extern crate smart_default; #[macro_use] extern crate failure; #[macro_use] extern crate serde_derive; #[macro_use] extern crate log; use std::path::{PathBuf, Path}; use std::collections::HashMap; use std::fs::{File, DirEntry, OpenOptions}; use std::io::{Read, Write}; use std::{env, fs, io}; use failure::Fallible; use clap; mod hash_dict; mod logging; mod bread; use bread::Bread; use crate::hash_dict::HashDict; use chrono::Datelike; use itertools::Itertools; #[derive(Serialize, Deserialize, Debug, SmartDefault, Clone)] #[serde(default)] pub struct AppConfig { #[default = "info"] logging: String, #[default = "https://www.ondrovo.com/bread"] web_url : String, } pub struct GalleryInfo { web_path: PathBuf, data_path: PathBuf, tpl_path: PathBuf, thumbs_path: PathBuf, base_url: String, templates: HashMap, image_hashes: hash_dict::HashDict, latest_year: i32, oldest_year: i32, } impl GalleryInfo { /// Read a named template from file, reusing from cache if possible fn template(&mut self, name: &str) -> Fallible { if let Some(text) = self.templates.get(name) { return Ok(text.clone()); } let mut tpl = String::new(); File::open(self.tpl_path.join(name))?.read_to_string(&mut tpl)?; self.templates.insert(name.to_string(), tpl.clone()); Ok(tpl) } } const CONFIG_FILE: &str = "breadgen.json"; fn main() -> Fallible<()> { let version = clap::crate_version!(); let clap = clap::App::new("Flowbox RT") .version(version) .arg( clap::Arg::with_name("config") .short("c") .long("config") .value_name("FILE") .help("Sets a custom config file (default: breadgen.json)") .takes_value(true), ); let clap = logging::add_clap_args(clap); let argv = clap.get_matches(); /* Load config */ let appcfg = { let confile = argv.value_of("config") .unwrap_or(CONFIG_FILE); println!("Bread gallery builder {}\nrun with -h for help", version); println!("config file: {}", confile); let buf = read_file(confile) .unwrap_or("{}".to_string()); let config: AppConfig = serde_json::from_str(&buf)?; let _ = logging::init(&config.logging, &argv, None); config }; let mut ginfo = { let cwd = env::current_dir()?; let web_path = cwd.join("web"); let data_path = web_path.join("data"); let tpl_path = web_path.join("templates"); let thumbs_path = web_path.join("thumbs"); GalleryInfo { web_path, data_path, tpl_path, thumbs_path, base_url: appcfg.web_url.clone(), templates: HashMap::new(), image_hashes: HashDict::load(cwd.join(".hashes.txt"))?, latest_year: 0, oldest_year: 0, } }; let mut bread_dirs: Vec = fs::read_dir(&ginfo.data_path)? .filter_map(Result::ok) .collect(); bread_dirs.sort_by(|x, y| { x.file_name().cmp(&y.file_name()) }); let mut breads: Vec = bread_dirs .iter() .filter_map(|p| Bread::parse(&ginfo.web_path, &p).ok()) .collect(); ginfo.latest_year = breads.last().unwrap().date.year(); ginfo.oldest_year = breads.first().unwrap().date.year(); for i in 0..breads.len() { let preceding = if i <= 0 { None } else { match breads.get(i - 1) { Some(b) => Some(b.to_link()), None => None } }; let following = match breads.get(i + 1) { Some(b) => Some(b.to_link()), None => None }; let cur = breads.get_mut(i).unwrap(); cur.compile(&mut ginfo, preceding, following)?; } let mut channel: rss::Channel = rss::ChannelBuilder::default() .title("Piggo's Bread Gallery") .link("https://www.ondrovo.com/bread") .description("Sourdough feed") .build() .unwrap(); ginfo.image_hashes.save(); // rss { let start = (breads.len() as i32 - 10).max(0) as usize; let mut channel_items = vec![]; for b in &mut breads[start..] { channel_items.push(b.rendered.rss_item.take().unwrap()); } info!("Generating feed..."); let f = OpenOptions::new() .write(true) .truncate(true) .create(true) .open(ginfo.web_path.join("feed.xml")) .unwrap(); channel.set_items(channel_items); channel.pretty_write_to(f, b' ', 2).unwrap(); } // main page { // make thumbs go from the newest to the oldest breads.reverse(); let oldest = ginfo.oldest_year; let latest = ginfo.latest_year; let base_url = ginfo.base_url.clone(); let year_pager = |year| { let mut buf = String::new(); for y in oldest..=latest { if year == y { buf.push_str(&format!("
  • {y}\n", y=y)); } else { if y == latest { buf.push_str(&format!("
  • {y}\n", u=base_url, y=y)); } else { buf.push_str(&format!("
  • {y}\n", u=base_url, y=y)); } } } buf }; for (year, year_breads) in &breads.iter().group_by(|b| b.date.year()) { let mut thumbs_buf = String::new(); for b in year_breads { thumbs_buf.push_str(&b.rendered.thumb); } info!("Building the gallery page"); let head = ginfo.template("_head.html")? .replace("{title}", "Piggo's breads"); let main = ginfo.template("index.html")? .replace("{breads}", thumbs_buf.trim()) .replace("{year_pager}", year_pager(year).trim()) .replace("{head}", head.trim()); let fname = if year == ginfo.latest_year { "index.html".to_string() } else { format!("{}.html", year) }; let mut f = OpenOptions::new() .write(true) .truncate(true) .create(true) .open(ginfo.web_path.join(fname)) .unwrap(); f.write(main.as_bytes()).unwrap(); } } Ok(()) } pub fn read_file>(path: P) -> io::Result { let path = path.as_ref(); let mut file = File::open(path)?; let mut buf = String::new(); file.read_to_string(&mut buf)?; Ok(buf) }