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. 9
      src/hash_dict.rs
  4. 450
      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)",
"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)",
"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-utils 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"markdown 0.2.0 (git+https://github.com/johannhof/markdown.rs)",

@ -13,3 +13,5 @@ image-utils = "0.2.0"
image = "0.21.0"
blake2 = "0.8.0"
base64 = "0.10.1"
failure = "0.1.5"

@ -1,8 +1,8 @@
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::Read;
use std::io::Write;
use std::io;
use std::path::PathBuf;
// file-stored hash map used to prevent needless image regenerating
@ -33,18 +33,19 @@ impl HashDict {
for l in lines {
let halves: Vec<&str> = l.split("\t").collect();
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)
}
pub fn get(&self, key : &str) -> Option<&String> {
pub fn get(&self, key: &str) -> Option<&String> {
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.any_change = true;
}

@ -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::io;
use std::fs;
use std::fs::DirEntry;
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
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;
use std::collections::HashMap;
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)]
struct Bread {
path: PathBuf,
@ -29,26 +41,183 @@ struct Bread {
note: String,
rss_note: String,
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 {
fn thumb_photo(&self) -> (&str, &str) {
let mut first_img : &PathBuf = self.images.get(0).unwrap();
fn compile(&mut self, config: &mut GalleryConfig, prev : Option<BreadLink>, next : Option<BreadLink>) -> 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_str().unwrap().contains("cover") {
first_img = im;
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();
let img_alt = first_img.file_name().unwrap().to_str().unwrap();
Ok(())
}
(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> {
let bpath = bread_dir.path();
let mut 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();
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 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 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(),
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
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 web_path = Path::new(&cwd).join("web");
let web_path = 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<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()));
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 {
if let Ok(b) = Bread::parse(&web_path, &bread_dir) {
breads.push(b);
}
for i in 0..breads.len() {
let preceding = if i <= 0 { None } else {
match breads.get(i - 1) {
Some(b) => Some(b.to_link()),
None => None
}
};
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 following = match breads.get(i + 1) {
Some(b) => Some(b.to_link()),
None => None
};
let mut thumbs = Vec::<String>::new();
let cur = breads.get_mut(i).unwrap();
let mut channel : Channel = ChannelBuilder::default()
cur.compile(&mut config, preceding, following)?;
}
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
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 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);
config.image_hashes.save();
thumbs.push(thumb);
let mut start = breads.len() as i32 - 10;
if start < 0 {
start = 0;
}
// Add to RSS
// 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>"));
let mut channel_items = vec![];
for b in &breads[start as usize..] {
channel_items.push(b.rendered.rss_item.clone().unwrap());
}
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());
println!("Generating feed...");
let f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("feed.xml")).unwrap();
channel.set_items(channel_items);
channel.pretty_write_to(f, b' ', 2).unwrap();
}
// Generate the detail page
// main 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());
// make thumbs go from the newest to the oldest
breads.reverse();
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 mut thumbs_buf = String::new();
for b in &breads {
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");
let main = main_tpl.replace("{breads}", &thumbs.join("").trim())
.replace("{head}", &head_tpl.replace("{title}", "Piggo's breads").trim());
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(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();
}
{
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();
}
Ok(())
}

@ -94,3 +94,9 @@ a {
#crumb a:hover {
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}">
</head>
<body>
<h1>{heading}</h1>
<h1>{prev}<a href="index.html">{heading}</a>{next}</h1>
<section class="BreadDetail">
<div class="note">

Loading…
Cancel
Save