You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
9.5 KiB
286 lines
9.5 KiB
5 years ago
|
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()
|
||
|
}
|