Ondřej Hruška преди 5 години
родител afeac2377f
ревизия 8fb340395f
Подписан от: MightyPork
GPG ключ ID: 2C5FD5035250423D
  1. 1
      Cargo.lock
  2. 2
      Cargo.toml
  3. 11
      src/hash_dict.rs
  4. 476
      src/main.rs
  5. 8
      web/assets/style.css
  6. Двоични данни
      web/data/2019-02-13/2019-02-13 21.56.35.jpg
  7. 1
      web/data/2019-02-13/note.txt
  8. Двоични данни
      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
@ -14,7 +14,7 @@ pub struct HashDict {
}
impl HashDict {
pub fn load(path: PathBuf) -> Result<HashDict, io::Error> {
pub fn load(path: PathBuf) -> Result<HashDict, io::Error> {
let mut hd = HashDict {
hashes: HashMap::new(),
path,
@ -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,243 +41,363 @@ struct Bread {
note: String,
rss_note: String,
images: Vec<PathBuf>,
rendered: BreadRendered,
}
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;
}
}
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();
//impl From<std::option::NoneError> for failure::Error {
// fn from(x: std::option::NoneError) -> Self {
// failure::err_msg("Expected something, found nothing.")
// }
//}
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(&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);
}
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,
images
});
}
#[derive(Debug)]
struct BreadLink {
label: String,
url: String,
}
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");
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();
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")
.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();
impl Bread {
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);
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;
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 = thumbs_path.join(&thumb_fname);
let thumb_relpath = thumb_path.strip_prefix(&web_path).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_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);
let image_real_path = config.web_path.join(img_path);
// Create the thumb
{
let mut img_file = fs::File::open(&image_real_path).unwrap();
let mut img_file = fs::File::open(&image_real_path)?;
let mut hasher = Blake2b::new();
io::copy(&mut img_file, &mut hasher).unwrap();
io::copy(&mut img_file, &mut hasher)?;
let hash = base64::encode(&hasher.result());
let hash_key = thumb_path.to_str().unwrap();
let old_hash = hashes.get(hash_key);
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).unwrap();
let im = image::open(&image_real_path)?;
let im = im.thumbnail(500, 500);
im.save(&thumb_path).unwrap();
im.save(&thumb_path)?;
hashes.put(hash_key.to_string(), hash);
config.image_hashes.put(hash_key.to_string(), hash);
}
}
// Prepare the thumb card for the gallery page
{
let thumb = thumb_tpl
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);
thumbs.push(thumb);
}
// Add to RSS
{
let image_url : String = channel.link().to_string() + "/" + &image_path_encoded;
let image_url: String = config.base_url.to_owned() + "/" + &image_path_encoded;
let link : String = channel.link().to_string() + "/" + &detail_file;
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(&bread.date).unwrap();
let dt = date_formatted.and_hms(12,0,0);
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 !rss_note.is_empty() {
descr.push_str(&(rss_note.to_string() + "<hr>"));
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, bread.images.len()));
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()
));
channel_items.push(ItemBuilder::default()
self.rendered.url = link.clone();
let item: rss::Item = ItemBuilder::default()
.title(date.clone())
.link(link.clone())
.link(link)
.description(descr)
.guid(guid)
.pub_date(dt.to_rfc2822())
.build().unwrap());
.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 = detail_tpl
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!("https://www.ondrovo.com/bread/{}", detail_file))
.replace("{thumb_url}", &format!("https://www.ondrovo.com/bread/thumbs/{}", thumb_fname))
.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 &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()))
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(web_path.join(detail_file)).unwrap();
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(())
}
hashes.save();
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(),
}
}
{
// 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());
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();
let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("index.html")).unwrap();
f.write(main.as_bytes()).unwrap();
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(&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);
}
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,
images,
rendered: BreadRendered {
thumb: "".to_string(),
detail: "".to_string(),
rss_item: None,
title: "".to_string(),
url: "".to_string(),
detail_fname: "".to_string()
},
});
}
}
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 = cwd.join("web");
let data_path = web_path.join("data");
let tpl_path = web_path.join("templates");
let thumbs_path = web_path.join("thumbs");
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> = bread_dirs
.iter()
.filter_map(|p| Bread::parse(&config.web_path, &p).ok())
.collect();
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 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 channel: Channel = ChannelBuilder::default()
.title("Piggo's Bread Gallery")
.link("https://www.ondrovo.com/bread")
.description("Sourdough feed")
.build()
.unwrap();
config.image_hashes.save();
let mut start = breads.len() as i32 - 10;
if start < 0 {
start = 0;
}
// rss
{
let mut channel_items = vec![];
for b in &breads[start as usize..] {
channel_items.push(b.rendered.rss_item.clone().unwrap());
}
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.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(())
}

@ -93,4 +93,10 @@ a {
#crumb a:hover {
color: #ccc;
}
}
.prev, .next {
font-size: 90%;
padding: 0 1em;
}

Binary file not shown.

След

Ширина:  |  Височина:  |  Големина: 2.0 MiB

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

Двоични данни
web/data/2019-02-18/bread.jpg

Binary file not shown.

След

Ширина:  |  Височина:  |  Големина: 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">

Зареждане…
Отказ
Запис