use crate::GalleryInfo; use std::path::{PathBuf, Path}; use percent_encoding::utf8_percent_encode; use std::{fs, io}; use blake2::{Blake2b, Digest}; use rss::{ItemBuilder, Guid}; use chrono::{TimeZone, Date, Utc, NaiveDate, Datelike}; use std::fs::{OpenOptions, DirEntry, File}; use std::io::{Read, Write}; use failure::Fallible; use std::borrow::Cow; #[derive(Debug)] pub struct BreadRendered { detail: String, title: String, url: String, detail_fname: String, pub thumb: String, pub rss_item: Option, } #[derive(Debug)] pub struct Bread { path: PathBuf, rel_path: PathBuf, pub date: chrono::NaiveDate, note: String, rss_note: String, images: Vec, pub rendered: BreadRendered, } #[derive(Debug)] pub struct BreadLink { label: String, url: String, } impl Bread { pub fn compile(&mut self, config: &mut GalleryInfo, prev : Option, next : Option) -> Fallible<()> { let date = self.date.format("%Y/%m/%d").to_string(); let date_slug = self.date.format("%Y-%m-%d").to_string(); let detail_file = date_slug.clone() + ".html"; println!("+ {}", date_slug); self.rendered.title = date.clone(); self.rendered.detail_fname = detail_file.clone(); // figure out the thumbnail pic let (img_path, img_alt) = { let mut first_img: &PathBuf = self.images.get(0).expect(&format!("No images for bread {}", date_slug)); for im in &self.images { if im.file_name().unwrap().to_string_lossy().contains("cover") { first_img = im; break; } } ( first_img.to_str().unwrap().to_owned(), first_img.file_name().unwrap().to_string_lossy().to_owned(), ) }; let (note, note_html) = if self.note.is_empty() { (Cow::Owned(String::new()), "".to_string()) } else { (Cow::Borrowed(&self.note), format!(r#"
{}
"#, self.note.trim())) }; let thumb_fname = date_slug.clone() + "." + Path::new(&img_path).extension().unwrap().to_str().unwrap(); let thumb_path = config.thumbs_path.join(&thumb_fname); let thumb_relpath = thumb_path.strip_prefix(&config.web_path)?; let image_path_encoded = urlencode(thumb_relpath.to_string_lossy()); let image_real_path = config.web_path.join(img_path); // Create the thumb { let mut img_file = fs::File::open(&image_real_path)?; let mut hasher = Blake2b::new(); io::copy(&mut img_file, &mut hasher)?; let hash = base64::encode(&hasher.result()); let hash_key = thumb_path.to_str().unwrap(); let old_hash = config.image_hashes.get(hash_key); if old_hash.is_none() || !old_hash.unwrap().eq(&hash) { println!("building thumb..."); let im = image::open(&image_real_path)?; let im = im.thumbnail(500, 500); im.save(&thumb_path)?; config.image_hashes.put(hash_key.to_string(), hash); } } // Prepare the thumb card for the gallery page { self.rendered.thumb = config .template("_thumb.html")? .replace("{detail_url}", &detail_file) .replace("{img_src}", &image_path_encoded) .replace("{img_alt}", &img_alt) .replace("{title}", &date); } // Add to RSS { let image_url: String = config.base_url.to_owned() + "/" + &image_path_encoded; let link: String = config.base_url.to_owned() + "/" + &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(&self.date).unwrap(); let dt = date_formatted.and_hms(12, 0, 0); let mut descr = String::new(); if !self.rss_note.is_empty() { descr.push_str(&self.rss_note); descr.push_str("
"); } descr.push_str(¬e); descr.push_str(&format!( "\"{}\"

Open the link for full-res photos ({} total)", image_url, img_alt, self.images.len() )); self.rendered.url = link.clone(); let item: rss::Item = ItemBuilder::default() .title(date.clone()) .link(link) .description(descr) .guid(guid) .pub_date(dt.to_rfc2822()) .build().unwrap(); self.rendered.rss_item = Some(item); } let head_tpl = config.template("_head.html")?; // Generate the detail page { let win_title = format!("Bread from {}", date); let byear = self.date.year(); let detail = config .template("detail.html")? .replace("{head}", &head_tpl.replace("{title}", &win_title)) .replace("{title}", &win_title) .replace("{date}", &date_slug); let detail = if byear == config.latest_year { detail.replace("{gallery_url}", "index.html") } else { detail.replace("{gallery_url}", &format!("{}.html", byear)) }; let detail = detail .replace("{url}", &format!("{}/{}", config.base_url, detail_file)) .replace( "{thumb_url}", &format!("{}/thumbs/{}", config.base_url, thumb_fname), ) .replace("{heading}", &date) .replace("{prev}", &(match prev { Some(b) => format!(r##"<"##, b.url, b.label), None => "".to_string() })) .replace("{next}", &(match next { Some(b) => format!(r##">"##, b.url, b.label), None => "".to_string() })) .replace("{note}", ¬e_html); let mut pics = String::new(); for img in &self.images { let src = urlencode(img.to_string_lossy()); pics.push_str(&format!( " \"Bread\n", src=&src )) } let detail = detail.replace("{images}", &pics.trim()); let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join(detail_file)).unwrap(); f.write(detail.as_bytes()).unwrap(); self.rendered.detail = detail; } Ok(()) } pub fn to_link(&self) -> BreadLink { BreadLink { label: self.date.format("%Y/%m/%d").to_string(), url: self.date.format("%Y-%m-%d.html").to_string(), } } pub fn parse(base_dir: &PathBuf, bread_dir: &DirEntry) -> Fallible { 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_string_lossy(); return name.ends_with(".jpg") || name.ends_with(".jpeg"); }) .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_string_lossy(), "%Y-%m-%d", ) .unwrap(), rel_path: bpath.strip_prefix(base_dir)?.to_path_buf(), path: bpath, note, rss_note, images, rendered: BreadRendered { thumb: "".to_string(), detail: "".to_string(), rss_item: None, title: "".to_string(), url: "".to_string(), detail_fname: "".to_string() }, }); } } pub fn urlencode<'a>(url : impl Into>) -> String { utf8_percent_encode(url.into().as_ref(), percent_encoding::DEFAULT_ENCODE_SET).to_string() }