bread gallery data and generator script
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bread-gallery/src/main.rs

272 lines
9.5 KiB

5 years ago
use std::env;
use std::io;
5 years ago
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;
5 years ago
#[derive(Debug)]
struct Bread {
path: PathBuf,
rel_path: PathBuf,
date: chrono::NaiveDate,
note: String,
rss_note: String,
5 years ago
images: Vec<PathBuf>,
}
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;
}
}
5 years ago
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<Bread, std::io::Error> {
let bpath = bread_dir.path();
let mut note = String::new();
let mut rss_note = String::new();
5 years ago
let mut note_path = bpath.join("note.txt");
let mut rss_note_path = bpath.join("rss.txt");
5 years ago
// 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");
}
5 years ago
if note_path.exists() {
let mut note_file = File::open(note_path)?;
note_file.read_to_string(&mut note)?;
note = markdown::to_html(&note);
}
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);
}
5 years ago
let mut bread_files: Vec<DirEntry> = 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,
5 years ago
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");
5 years ago
let mut bread_dirs: Vec<DirEntry> = 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<Bread> = 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();
5 years ago
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();
5 years ago
File::open(tpl_path.join("detail.html")).unwrap().read_to_string(&mut detail_tpl).unwrap();
let mut thumbs = Vec::<String>::new();
5 years ago
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::<Item>::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
5 years ago
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";
5 years ago
println!("+ {}", date_slug);
5 years ago
let (img_path, img_alt) = bread.thumb_photo();
let note = if bread.note.is_empty() { "<i>There's no note about this bread.</i>" } 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...");
5 years ago
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);
}
5 years ago
// 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<Utc> = 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() + "<hr>"));
}
descr.push_str(note);
descr.push_str(&format!("<img src=\"{}\" alt=\"{}\"><p><i>Open the link for full-res photos ({} total)</i>", 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());
}
5 years ago
// Generate the detail page
5 years ago
{
let win_title = format!("Bread from {}", date);
5 years ago
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());
5 years ago
let mut pics = String::new();
for img in &bread.images {
pics.push_str(&format!(" <a href=\"{src}\"><img src=\"{src}\"></a>\n", src=&utf8_percent_encode(img.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string()))
5 years ago
}
let detail = detail.replace("{images}", &pics.trim());
5 years ago
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();
5 years ago
{
// 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());
5 years ago
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();
}
5 years ago
}