big refactor

This commit is contained in:
2019-02-21 00:33:11 +01:00
parent afeac2377f
commit 8fb340395f
10 changed files with 333 additions and 187 deletions
Generated
+1
View File
@@ -82,6 +82,7 @@ dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"image 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
"image-utils 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "image-utils 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"markdown 0.2.0 (git+https://github.com/johannhof/markdown.rs)", "markdown 0.2.0 (git+https://github.com/johannhof/markdown.rs)",
+2
View File
@@ -13,3 +13,5 @@ image-utils = "0.2.0"
image = "0.21.0" image = "0.21.0"
blake2 = "0.8.0" blake2 = "0.8.0"
base64 = "0.10.1" base64 = "0.10.1"
failure = "0.1.5"
+5 -4
View File
@@ -1,8 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::io;
use std::path::PathBuf; use std::path::PathBuf;
// file-stored hash map used to prevent needless image regenerating // file-stored hash map used to prevent needless image regenerating
@@ -33,18 +33,19 @@ impl HashDict {
for l in lines { for l in lines {
let halves: Vec<&str> = l.split("\t").collect(); let halves: Vec<&str> = l.split("\t").collect();
if halves.len() == 2 { if halves.len() == 2 {
hd.hashes.insert(halves[0].to_string(), halves[1].to_string()); hd.hashes
.insert(halves[0].to_string(), halves[1].to_string());
} }
} }
Ok(hd) Ok(hd)
} }
pub fn get(&self, key : &str) -> Option<&String> { pub fn get(&self, key: &str) -> Option<&String> {
self.hashes.get(key) self.hashes.get(key)
} }
pub fn put(&mut self, key : String, value: String) { pub fn put(&mut self, key: String, value: String) {
self.hashes.insert(key, value); self.hashes.insert(key, value);
self.any_change = true; self.any_change = true;
} }
+300 -168
View File
@@ -1,26 +1,38 @@
use blake2::{Blake2b, Digest};
use chrono;
use chrono::offset::TimeZone;
use chrono::Date;
use chrono::NaiveDate;
use chrono::Utc;
use failure::Fallible;
use image_utils;
use markdown;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use rss::{Channel, ChannelBuilder, Guid, ItemBuilder};
use std::env; use std::env;
use std::io;
use std::fs; use std::fs;
use std::fs::DirEntry; use std::fs::DirEntry;
use std::fs::File; use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf}; 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; use base64;
use std::collections::HashMap;
mod hash_dict; mod hash_dict;
#[derive(Debug)]
struct BreadRendered {
thumb: String,
detail: String,
rss_item: Option<rss::Item>,
title: String,
url: String,
detail_fname: String,
}
#[derive(Debug)] #[derive(Debug)]
struct Bread { struct Bread {
path: PathBuf, path: PathBuf,
@@ -29,26 +41,183 @@ struct Bread {
note: String, note: String,
rss_note: String, rss_note: String,
images: Vec<PathBuf>, images: Vec<PathBuf>,
rendered: BreadRendered,
}
//impl From<std::option::NoneError> for failure::Error {
// fn from(x: std::option::NoneError) -> Self {
// failure::err_msg("Expected something, found nothing.")
// }
//}
#[derive(Debug)]
struct BreadLink {
label: String,
url: String,
} }
impl Bread { impl Bread {
fn thumb_photo(&self) -> (&str, &str) { fn compile(&mut self, config: &mut GalleryConfig, prev : Option<BreadLink>, next : Option<BreadLink>) -> Fallible<()> {
let mut first_img : &PathBuf = self.images.get(0).unwrap(); 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 { for im in &self.images {
if im.file_name().unwrap().to_str().unwrap().contains("cover") { if im.file_name().unwrap().to_str().unwrap().contains("cover") {
first_img = im; first_img = im;
break; break;
} }
} }
(
first_img.to_str().unwrap().to_owned(),
first_img.file_name().unwrap().to_str().unwrap().to_owned(),
)
};
let img_path = first_img.to_str().unwrap(); let note = if self.note.is_empty() {
let img_alt = first_img.file_name().unwrap().to_str().unwrap(); "<i>There's no note about this bread.</i>"
} else {
&self.note
};
(img_path, img_alt) 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 =
utf8_percent_encode(thumb_relpath.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string();
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);
}
} }
fn parse(base_dir : &PathBuf, bread_dir : &DirEntry) -> Result<Bread, std::io::Error> { // 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<Utc> = 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("<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,
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 detail = config
.template("detail.html")?
.replace("{head}", &head_tpl.replace("{title}", &win_title))
.replace("{title}", &win_title)
.replace("{date}", &date_slug)
.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!("<a class=\"prev\" href=\"{}\" title=\"{}\">&lt;</a>", b.url, b.label),
None => "".to_string()
}))
.replace("{next}", &(match next {
Some(b) => format!("<a class=\"next\" href=\"{}\" title=\"{}\">&gt;</a>", b.url, b.label),
None => "".to_string()
}))
.replace("{note}", note.trim());
let mut pics = String::new();
for img in &self.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()
))
}
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(())
}
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(),
}
}
fn parse(base_dir: &PathBuf, bread_dir: &DirEntry) -> Result<Bread, std::io::Error> {
let bpath = bread_dir.path(); let bpath = bread_dir.path();
let mut note = String::new(); let mut note = String::new();
let mut rss_note = String::new(); let mut rss_note = String::new();
@@ -80,192 +249,155 @@ impl Bread {
let mut bread_files: Vec<DirEntry> = fs::read_dir(&bpath)?.map(|e| e.unwrap()).collect(); 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())); bread_files.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
let images = bread_files.iter().filter(|&f| { let images = bread_files
.iter()
.filter(|&f| {
let fname = f.file_name(); let fname = f.file_name();
let name = fname.to_str().unwrap(); let name = fname.to_str().unwrap();
return return name.ends_with(".png")
name.ends_with(".png") || || name.ends_with(".jpg")
name.ends_with(".jpg") || || name.ends_with(".jpeg")
name.ends_with(".jpeg") || || name.ends_with(".gif");
name.ends_with(".gif"); })
}).map(|x| x.path().strip_prefix(base_dir).unwrap().to_path_buf()).collect(); .map(|x| x.path().strip_prefix(base_dir).unwrap().to_path_buf())
.collect();
return Ok(Bread { return Ok(Bread {
date: NaiveDate::parse_from_str(bpath.file_name().unwrap().to_str().unwrap(), "%Y-%m-%d").unwrap(), 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(), rel_path: bpath.strip_prefix(base_dir).unwrap().to_path_buf(),
path: bpath, path: bpath,
note, note,
rss_note, rss_note,
images images,
rendered: BreadRendered {
thumb: "".to_string(),
detail: "".to_string(),
rss_item: None,
title: "".to_string(),
url: "".to_string(),
detail_fname: "".to_string()
},
}); });
} }
} }
fn main() { struct GalleryConfig {
web_path: PathBuf,
data_path: PathBuf,
tpl_path: PathBuf,
thumbs_path: PathBuf,
base_url: String,
templates: HashMap<String, String>,
image_hashes: hash_dict::HashDict,
}
impl GalleryConfig {
fn template(&mut self, name: &str) -> Fallible<String> {
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)
}
}
fn main() -> Fallible<()> {
let mut config = {
let cwd = env::current_dir().unwrap(); let cwd = env::current_dir().unwrap();
let web_path = Path::new(&cwd).join("web"); let web_path = cwd.join("web");
let data_path = web_path.join("data"); let data_path = web_path.join("data");
let tpl_path = web_path.join("templates"); let tpl_path = web_path.join("templates");
let thumbs_path = web_path.join("thumbs"); let thumbs_path = web_path.join("thumbs");
let mut bread_dirs: Vec<DirEntry> = fs::read_dir(&data_path).unwrap().map(|e| e.unwrap()).collect(); GalleryConfig {
web_path,
data_path,
tpl_path,
thumbs_path,
base_url: "https://www.ondrovo.com/bread".into(),
templates: HashMap::new(),
image_hashes: hash_dict::HashDict::load(cwd.join(".hashes.txt"))?,
}
};
let mut bread_dirs: Vec<DirEntry> = fs::read_dir(&config.data_path)?.filter_map(|e| e.ok()).collect();
bread_dirs.sort_by(|x, y| x.file_name().cmp(&y.file_name())); bread_dirs.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
let mut breads : Vec<Bread> = Vec::new(); let mut breads: Vec<Bread> = bread_dirs
.iter()
.filter_map(|p| Bread::parse(&config.web_path, &p).ok())
.collect();
for bread_dir in bread_dirs { for i in 0..breads.len() {
if let Ok(b) = Bread::parse(&web_path, &bread_dir) { let preceding = if i <= 0 { None } else {
breads.push(b); 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 config, preceding, following)?;
} }
let mut main_tpl = String::new(); let mut channel: Channel = ChannelBuilder::default()
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::<String>::new();
let mut channel : Channel = ChannelBuilder::default()
.title("Piggo's Bread Gallery") .title("Piggo's Bread Gallery")
.link("https://www.ondrovo.com/bread") .link("https://www.ondrovo.com/bread")
.description("Sourdough feed") .description("Sourdough feed")
.build() .build()
.unwrap(); .unwrap();
let mut channel_items = Vec::<Item>::new(); config.image_hashes.save();
let mut hashes = hash_dict::HashDict::load(cwd.join(".hashes.txt")).unwrap(); let mut start = breads.len() as i32 - 10;
if start < 0 {
start = 0;
}
// TODO separate thumbs by year and generate per-year pages // rss
// 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() { "<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 channel_items = vec![];
let mut hasher = Blake2b::new(); for b in &breads[start as usize..] {
io::copy(&mut img_file, &mut hasher).unwrap(); channel_items.push(b.rendered.rss_item.clone().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<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());
}
// 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!(" <a href=\"{src}\"><img src=\"{src}\"></a>\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..."); println!("Generating feed...");
let f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("feed.xml")).unwrap(); let f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("feed.xml")).unwrap();
channel.set_items(channel_items); channel.set_items(channel_items);
channel.pretty_write_to(f, b' ', 2).unwrap(); channel.pretty_write_to(f, b' ', 2).unwrap();
} }
// main page
{
// make thumbs go from the newest to the oldest
breads.reverse();
let mut thumbs_buf = String::new();
for b in &breads {
thumbs_buf.push_str(&b.rendered.thumb);
}
println!("Building the gallery page");
let main = config.template("index.html")?.replace("{breads}", &thumbs_buf.trim())
.replace("{head}", config.template("_head.html")?.replace("{title}", "Piggo's breads").trim());
let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("index.html")).unwrap();
f.write(main.as_bytes()).unwrap();
}
Ok(())
} }
+6
View File
@@ -94,3 +94,9 @@ a {
#crumb a:hover { #crumb a:hover {
color: #ccc; color: #ccc;
} }
.prev, .next {
font-size: 90%;
padding: 0 1em;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+1
View File
@@ -0,0 +1 @@
a little flat but A+ taste and cronch
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+3
View File
@@ -0,0 +1,3 @@
The previous bread came out a bit tough. I run out of bread flour then, so it was made from fine flour. May be why.
This one is almost perfect, but it's still turning tough quickly. I'm investigating ways to keep it fresh longer... If you have a tip, please let me know!
+1 -1
View File
@@ -25,7 +25,7 @@
<meta itemprop="image" content="{thumb_url}"> <meta itemprop="image" content="{thumb_url}">
</head> </head>
<body> <body>
<h1>{heading}</h1> <h1>{prev}<a href="index.html">{heading}</a>{next}</h1>
<section class="BreadDetail"> <section class="BreadDetail">
<div class="note"> <div class="note">