parent
af687cb8df
commit
6bcebd115c
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@ |
|||||||
#!/bin/bash |
#!/bin/bash |
||||||
|
|
||||||
#cargo run --release |
cargo run --release |
||||||
target/release/bread |
#target/release/bread |
||||||
cp -R web/* /home/ondra/devel/ondrovo/blog/base/bread/ |
cp -R web/* /home/ondra/devel/ondrovo/blog/base/bread/ |
||||||
|
@ -0,0 +1,285 @@ |
|||||||
|
use crate::GalleryInfo; |
||||||
|
use std::path::{PathBuf, Path}; |
||||||
|
use percent_encoding::utf8_percent_encode; |
||||||
|
use std::{fs, io}; |
||||||
|
use blake2::{Blake2b, Digest}; |
||||||
|
use rss::{ItemBuilder, Guid}; |
||||||
|
use chrono::{TimeZone, Date, Utc, NaiveDate, Datelike}; |
||||||
|
use std::fs::{OpenOptions, DirEntry, File}; |
||||||
|
use std::io::{Read, Write}; |
||||||
|
use failure::Fallible; |
||||||
|
use std::borrow::Cow; |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct BreadRendered { |
||||||
|
detail: String, |
||||||
|
title: String, |
||||||
|
url: String, |
||||||
|
detail_fname: String, |
||||||
|
pub thumb: String, |
||||||
|
pub rss_item: Option<rss::Item>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Bread { |
||||||
|
path: PathBuf, |
||||||
|
rel_path: PathBuf, |
||||||
|
pub date: chrono::NaiveDate, |
||||||
|
note: String, |
||||||
|
rss_note: String, |
||||||
|
images: Vec<PathBuf>, |
||||||
|
pub rendered: BreadRendered, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct BreadLink { |
||||||
|
label: String, |
||||||
|
url: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl Bread { |
||||||
|
pub fn compile(&mut self, config: &mut GalleryInfo, 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_string_lossy().contains("cover") { |
||||||
|
first_img = im; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
( |
||||||
|
first_img.to_str().unwrap().to_owned(), |
||||||
|
first_img.file_name().unwrap().to_string_lossy().to_owned(), |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
let (note, note_html) = if self.note.is_empty() { |
||||||
|
(Cow::Owned(String::new()), "<!-- There's no note about this bread. -->".to_string()) |
||||||
|
} else { |
||||||
|
(Cow::Borrowed(&self.note), format!(r#"<div class="note">{}</div>"#, self.note.trim())) |
||||||
|
}; |
||||||
|
|
||||||
|
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 = urlencode(thumb_relpath.to_string_lossy()); |
||||||
|
|
||||||
|
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(¬e); |
||||||
|
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 byear = self.date.year(); |
||||||
|
let detail = config |
||||||
|
.template("detail.html")? |
||||||
|
.replace("{head}", &head_tpl.replace("{title}", &win_title)) |
||||||
|
.replace("{title}", &win_title) |
||||||
|
.replace("{date}", &date_slug); |
||||||
|
|
||||||
|
let detail = if byear == config.latest_year { |
||||||
|
detail.replace("{gallery_url}", "index.html") |
||||||
|
} else { |
||||||
|
detail.replace("{gallery_url}", &format!("{}.html", byear)) |
||||||
|
}; |
||||||
|
|
||||||
|
let detail = detail |
||||||
|
.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!(r##"<a class="#prev" href="{}" title="{}"><</a>"##, b.url, b.label), |
||||||
|
None => "".to_string() |
||||||
|
})) |
||||||
|
.replace("{next}", &(match next { |
||||||
|
Some(b) => format!(r##"<a class="#next" href="{}" title="{}">></a>"##, b.url, b.label), |
||||||
|
None => "".to_string() |
||||||
|
})) |
||||||
|
.replace("{note}", ¬e_html); |
||||||
|
|
||||||
|
let mut pics = String::new(); |
||||||
|
for img in &self.images { |
||||||
|
let src = urlencode(img.to_string_lossy()); |
||||||
|
pics.push_str(&format!( |
||||||
|
" <a href=\"{src}\"><img alt=\"Bread photo {src}\" src=\"{src}\"></a>\n", |
||||||
|
src=&src |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
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(()) |
||||||
|
} |
||||||
|
|
||||||
|
pub 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(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn parse(base_dir: &PathBuf, bread_dir: &DirEntry) -> Fallible<Bread> { |
||||||
|
let bpath = bread_dir.path(); |
||||||
|
let mut note = String::new(); |
||||||
|
let mut rss_note = String::new(); |
||||||
|
|
||||||
|
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(¬e); |
||||||
|
} |
||||||
|
|
||||||
|
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_string_lossy(); |
||||||
|
return name.ends_with(".jpg") || name.ends_with(".jpeg"); |
||||||
|
}) |
||||||
|
.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_string_lossy(), |
||||||
|
"%Y-%m-%d", |
||||||
|
) |
||||||
|
.unwrap(), |
||||||
|
rel_path: bpath.strip_prefix(base_dir)?.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() |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn urlencode<'a>(url : impl Into<Cow<'a, str>>) -> String { |
||||||
|
utf8_percent_encode(url.into().as_ref(), percent_encoding::DEFAULT_ENCODE_SET).to_string() |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
use failure::{bail, Fallible}; |
||||||
|
|
||||||
|
pub const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; |
||||||
|
|
||||||
|
/// 3rd-party libraries that produce log spam - we set these to a fixed higher level
|
||||||
|
/// to allow using e.g. TRACE without drowing our custom messages
|
||||||
|
pub const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"]; |
||||||
|
|
||||||
|
pub fn add_clap_args<'b, 'a: 'b>(clap : clap::App<'a, 'b>) -> clap::App<'a, 'b> { |
||||||
|
clap |
||||||
|
.arg(clap::Arg::with_name("verbose") |
||||||
|
.short("v").multiple(true) |
||||||
|
.help("Increase logging verbosity (repeat to increase)")) |
||||||
|
.arg(clap::Arg::with_name("log-level") |
||||||
|
.short("l").long("log") |
||||||
|
.takes_value(true).value_name("LEVEL") |
||||||
|
.validator(|s| { |
||||||
|
if LOG_LEVELS.contains(&s.as_str()) { return Ok(()); } |
||||||
|
Err(format!("Bad log level: {}", s)) |
||||||
|
}) |
||||||
|
.help("Set logging verbosity (error,warning,info,debug,trace)")) |
||||||
|
} |
||||||
|
|
||||||
|
/// Initialize logging, using `level` as the base if not changed via command-line
|
||||||
|
/// arguments
|
||||||
|
pub fn init<'a, 'b>(mut level: &'a str, |
||||||
|
argv: &'a clap::ArgMatches, |
||||||
|
suppress: Option<&'b [&str]>) -> Fallible<&'a str> { |
||||||
|
if !LOG_LEVELS.contains(&level) { |
||||||
|
bail!("Invalid default log level: {}", level); |
||||||
|
} |
||||||
|
|
||||||
|
/* env RUST_LOG overrides default if set, but can be changed by CLI args */ |
||||||
|
let env_level = option_env!("RUST_LOG").unwrap_or(""); //env_logger::DEFAULT_FILTER_ENV
|
||||||
|
if !env_level.is_empty() { |
||||||
|
level = env_level; |
||||||
|
|
||||||
|
if !LOG_LEVELS.contains(&level) { |
||||||
|
bail!("Invalid env log level: {}", level); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* Explicitly requested level */ |
||||||
|
if let Some(l) = argv.value_of("log-level") { |
||||||
|
level = l; // validated by clap
|
||||||
|
} |
||||||
|
|
||||||
|
/* Verbosity increased */ |
||||||
|
if argv.is_present("verbose") { |
||||||
|
// bump verbosity if -v's are present
|
||||||
|
let pos = LOG_LEVELS |
||||||
|
.iter() |
||||||
|
.position(|x| x == &level) |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
level = match LOG_LEVELS |
||||||
|
.iter() |
||||||
|
.nth(pos + argv.occurrences_of("verbose") as usize) |
||||||
|
{ |
||||||
|
Some(new_level) => new_level, |
||||||
|
None => "trace", |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
let env = env_logger::Env::default().default_filter_or(level); |
||||||
|
let mut builder = env_logger::Builder::from_env(env); |
||||||
|
|
||||||
|
builder.format_timestamp_millis(); |
||||||
|
|
||||||
|
// set logging level for spammy libs. Ensure the configured log level is not exceeded
|
||||||
|
let mut lib_level = log::LevelFilter::Info; |
||||||
|
if level == "warn" { |
||||||
|
lib_level = log::LevelFilter::Warn; |
||||||
|
} else if level == "error" { |
||||||
|
lib_level = log::LevelFilter::Error; |
||||||
|
} |
||||||
|
|
||||||
|
for lib in suppress.unwrap_or(&SPAMMY_LIBS) { |
||||||
|
builder.filter_module(lib, lib_level); |
||||||
|
} |
||||||
|
|
||||||
|
builder.init(); |
||||||
|
|
||||||
|
Ok(level) |
||||||
|
} |
Loading…
Reference in new issue