big refactor

master
Ondřej Hruška 6 years ago
parent afeac2377f
commit 8fb340395f
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 1
      Cargo.lock
  2. 2
      Cargo.toml
  3. 5
      src/hash_dict.rs
  4. 446
      src/main.rs
  5. 6
      web/assets/style.css
  6. BIN
      web/data/2019-02-13/2019-02-13 21.56.35.jpg
  7. 1
      web/data/2019-02-13/note.txt
  8. BIN
      web/data/2019-02-18/bread.jpg
  9. 3
      web/data/2019-02-18/note.txt
  10. 2
      web/templates/detail.html

1
Cargo.lock generated

@ -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)",

@ -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"

@ -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,7 +33,8 @@ 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());
} }
} }

@ -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,23 +41,180 @@ 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 note = if self.note.is_empty() {
"<i>There's no note about this bread.</i>"
} else {
&self.note
};
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);
}
}
// 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;
}
let img_path = first_img.to_str().unwrap(); Ok(())
let img_alt = first_img.file_name().unwrap().to_str().unwrap(); }
(img_path, img_alt) 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> { fn parse(base_dir: &PathBuf, bread_dir: &DirEntry) -> Result<Bread, std::io::Error> {
@ -80,55 +249,110 @@ 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 mut main_tpl = String::new(); let cur = breads.get_mut(i).unwrap();
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(); cur.compile(&mut config, preceding, following)?;
}
let mut channel: Channel = ChannelBuilder::default() let mut channel: Channel = ChannelBuilder::default()
.title("Piggo's Bread Gallery") .title("Piggo's Bread Gallery")
@ -137,135 +361,43 @@ fn main() {
.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();
// TODO separate thumbs by year and generate per-year pages
// TODO limit RSS to last N breads
for bread in &breads { let mut start = breads.len() as i32 - 10;
let date = bread.date.format("%Y/%m/%d").to_string(); if start < 0 {
let date_slug = bread.date.format("%Y-%m-%d").to_string(); start = 0;
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 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 // rss
{ {
let thumb = thumb_tpl let mut channel_items = vec![];
.replace("{detail_url}", &detail_file) for b in &breads[start as usize..] {
.replace("{img_src}", &image_path_encoded) channel_items.push(b.rendered.rss_item.clone().unwrap());
.replace("{img_alt}", &img_alt)
.replace("{title}", &date);
thumbs.push(thumb);
} }
// Add to RSS println!("Generating feed...");
{ let f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("feed.xml")).unwrap();
let image_url : String = channel.link().to_string() + "/" + &image_path_encoded; channel.set_items(channel_items);
channel.pretty_write_to(f, b' ', 2).unwrap();
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 // main page
{ {
let win_title = format!("Bread from {}", date); // make thumbs go from the newest to the oldest
breads.reverse();
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(); let mut thumbs_buf = String::new();
for img in &bread.images { for b in &breads {
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())) thumbs_buf.push_str(&b.rendered.thumb);
} }
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"); println!("Building the gallery page");
let main = main_tpl.replace("{breads}", &thumbs.join("").trim()) let main = config.template("index.html")?.replace("{breads}", &thumbs_buf.trim())
.replace("{head}", &head_tpl.replace("{title}", "Piggo's breads").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(web_path.join("index.html")).unwrap(); 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(); f.write(main.as_bytes()).unwrap();
} }
{ Ok(())
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();
}
} }

@ -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

@ -0,0 +1 @@
a little flat but A+ taste and cronch

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

@ -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!

@ -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">

Loading…
Cancel
Save