use std::env; use std::io; use std::fs; use std::fs::DirEntry; use std::fs::File; use std::io::prelude::*; use std::path::{Path, PathBuf}; use chrono; use chrono::NaiveDate; use markdown; use std::fs::OpenOptions; use rss::{Channel, ChannelBuilder, Item, ItemBuilder, Guid}; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use image_utils; use chrono::offset::TimeZone; use chrono::Date; use chrono::Utc; use blake2::{Digest,Blake2b}; use base64; mod hash_dict; #[derive(Debug)] struct Bread { path: PathBuf, rel_path: PathBuf, date: chrono::NaiveDate, note: String, rss_note: String, images: Vec, } impl Bread { fn thumb_photo(&self) -> (&str, &str) { let mut first_img : &PathBuf = self.images.get(0).unwrap(); for im in &self.images { if im.file_name().unwrap().to_str().unwrap().contains("cover") { first_img = im; break; } } let img_path = first_img.to_str().unwrap(); let img_alt = first_img.file_name().unwrap().to_str().unwrap(); (img_path, img_alt) } fn parse(base_dir : &PathBuf, bread_dir : &DirEntry) -> Result { let bpath = bread_dir.path(); let mut note = String::new(); let mut rss_note = String::new(); let mut note_path = bpath.join("note.txt"); let mut rss_note_path = bpath.join("rss.txt"); // try a md one as a fallback if !note_path.exists() { note_path = bpath.join("note.md"); } if !rss_note_path.exists() { rss_note_path = bpath.join("rss.md"); } if note_path.exists() { let mut note_file = File::open(note_path)?; note_file.read_to_string(&mut note)?; note = markdown::to_html(¬e); } if rss_note_path.exists() { let mut note_file = File::open(rss_note_path)?; note_file.read_to_string(&mut rss_note)?; rss_note = markdown::to_html(&rss_note); } let mut bread_files: Vec = fs::read_dir(&bpath)?.map(|e| e.unwrap()).collect(); bread_files.sort_by(|x, y| x.file_name().cmp(&y.file_name())); let images = bread_files.iter().filter(|&f| { let fname = f.file_name(); let name = fname.to_str().unwrap(); return name.ends_with(".png") || name.ends_with(".jpg") || name.ends_with(".jpeg") || name.ends_with(".gif"); }).map(|x| x.path().strip_prefix(base_dir).unwrap().to_path_buf()).collect(); return Ok(Bread { date: NaiveDate::parse_from_str(bpath.file_name().unwrap().to_str().unwrap(), "%Y-%m-%d").unwrap(), rel_path: bpath.strip_prefix(base_dir).unwrap().to_path_buf(), path: bpath, note, rss_note, images }); } } fn main() { let cwd = env::current_dir().unwrap(); let web_path = Path::new(&cwd).join("web"); let data_path = web_path.join("data"); let tpl_path = web_path.join("templates"); let thumbs_path = web_path.join("thumbs"); let mut bread_dirs: Vec = fs::read_dir(&data_path).unwrap().map(|e| e.unwrap()).collect(); bread_dirs.sort_by(|x, y| x.file_name().cmp(&y.file_name())); let mut breads : Vec = Vec::new(); for bread_dir in bread_dirs { if let Ok(b) = Bread::parse(&web_path, &bread_dir) { breads.push(b); } } let mut main_tpl = String::new(); let mut thumb_tpl = String::new(); let mut detail_tpl = String::new(); let mut head_tpl = String::new(); File::open(tpl_path.join("index.html")).unwrap().read_to_string(&mut main_tpl).unwrap(); File::open(tpl_path.join("_thumb.html")).unwrap().read_to_string(&mut thumb_tpl).unwrap(); File::open(tpl_path.join("_head.html")).unwrap().read_to_string(&mut head_tpl).unwrap(); File::open(tpl_path.join("detail.html")).unwrap().read_to_string(&mut detail_tpl).unwrap(); let mut thumbs = Vec::::new(); let mut channel : Channel = ChannelBuilder::default() .title("Piggo's Bread Gallery") .link("https://www.ondrovo.com/bread") .description("Sourdough feed") .build() .unwrap(); let mut channel_items = Vec::::new(); let mut hashes = hash_dict::HashDict::load(cwd.join(".hashes.txt")).unwrap(); // TODO separate thumbs by year and generate per-year pages // TODO limit RSS to last N breads for bread in &breads { let date = bread.date.format("%Y/%m/%d").to_string(); let date_slug = bread.date.format("%Y-%m-%d").to_string(); let detail_file = date_slug.clone() + ".html"; println!("+ {}", date_slug); let (img_path, img_alt) = bread.thumb_photo(); let note = if bread.note.is_empty() { "There's no note about this bread." } else { &bread.note }; let rss_note = &bread.rss_note; let thumb_fname = date_slug.clone() + "." + Path::new(&img_path).extension().unwrap().to_str().unwrap(); let thumb_path = thumbs_path.join(&thumb_fname); let thumb_relpath = thumb_path.strip_prefix(&web_path).unwrap(); let image_path_encoded = utf8_percent_encode(thumb_relpath.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string(); // TODO keep the original path in bread so we dont have to reconstruct it here let image_real_path = web_path.join(img_path); // Create the thumb { let mut img_file = fs::File::open(&image_real_path).unwrap(); let mut hasher = Blake2b::new(); io::copy(&mut img_file, &mut hasher).unwrap(); let hash = base64::encode(&hasher.result()); let hash_key = thumb_path.to_str().unwrap(); let old_hash = hashes.get(hash_key); if old_hash.is_none() || !old_hash.unwrap().eq(&hash) { println!("building thumb..."); let im = image::open(&image_real_path).unwrap(); let im = im.thumbnail(500, 500); im.save(&thumb_path).unwrap(); hashes.put(hash_key.to_string(), hash); } } // Prepare the thumb card for the gallery page { let thumb = thumb_tpl .replace("{detail_url}", &detail_file) .replace("{img_src}", &image_path_encoded) .replace("{img_alt}", &img_alt) .replace("{title}", &date); thumbs.push(thumb); } // Add to RSS { let image_url : String = channel.link().to_string() + "/" + &image_path_encoded; let link : String = channel.link().to_string() + "/" + &detail_file; let mut guid = Guid::default(); guid.set_value(link.clone()); guid.set_permalink(true); let date_formatted : Date = chrono::Utc.from_local_date(&bread.date).unwrap(); let dt = date_formatted.and_hms(12,0,0); let mut descr = String::new(); if !rss_note.is_empty() { descr.push_str(&(rss_note.to_string() + "
")); } descr.push_str(note); descr.push_str(&format!("\"{}\"

Open the link for full-res photos ({} total)", image_url, img_alt, bread.images.len())); channel_items.push(ItemBuilder::default() .title(date.clone()) .link(link.clone()) .description(descr) .guid(guid) .pub_date(dt.to_rfc2822()) .build().unwrap()); } // Generate the detail page { let win_title = format!("Bread from {}", date); let detail = detail_tpl .replace("{head}", &head_tpl.replace("{title}", &win_title)) .replace("{title}", &win_title) .replace("{date}", &date_slug) .replace("{url}", &format!("https://www.ondrovo.com/bread/{}", detail_file)) .replace("{thumb_url}", &format!("https://www.ondrovo.com/bread/thumbs/{}", thumb_fname)) .replace("{heading}", &date) .replace("{note}", note.trim()); let mut pics = String::new(); for img in &bread.images { pics.push_str(&format!(" \n", src=&utf8_percent_encode(img.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string())) } let detail = detail.replace("{images}", &pics.trim()); let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join(detail_file)).unwrap(); f.write(detail.as_bytes()).unwrap(); } } hashes.save(); { // make thumbs go from the newest to the oldest thumbs.reverse(); println!("Building the gallery page"); let main = main_tpl.replace("{breads}", &thumbs.join("").trim()) .replace("{head}", &head_tpl.replace("{title}", "Piggo's breads").trim()); let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("index.html")).unwrap(); f.write(main.as_bytes()).unwrap(); } { println!("Generating feed..."); let f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("feed.xml")).unwrap(); channel.set_items(channel_items); channel.pretty_write_to(f, b' ', 2).unwrap(); } }