@ -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,23 +41,180 @@ 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=\"{}\"><</a>" , b . url , b . label ) ,
None = > "" . to_string ( )
} ) )
. replace ( "{next}" , & ( match next {
Some ( b ) = > format! ( "<a class=\"next\" href=\"{}\" title=\"{}\">></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 > {
@ -80,55 +249,110 @@ 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 following = 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 cur = breads . get_mut ( i ) . unwrap ( ) ;
let mut thumbs = Vec ::< String > ::new ( ) ;
cur . compile ( & mut config , preceding , following ) ? ;
}
let mut channel : Channel = ChannelBuilder ::default ( )
. title ( "Piggo's Bread Gallery" )
@ -137,135 +361,43 @@ fn main() {
. 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
config . image_hashes . save ( ) ;
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 ) ;
}
let mut start = breads . len ( ) as i32 - 10 ;
if start < 0 {
start = 0 ;
}
// Prepare the thumb card for the gallery page
// rss
{
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 ) ;
let mut channel_items = vec! [ ] ;
for b in & breads [ start as usize .. ] {
channel_items . push ( b . rendered . rss_item . clone ( ) . unwrap ( ) ) ;
}
// 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 ( ) ) ;
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 . image s {
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 ( ( ) )
}