diff --git a/src/main.rs b/src/main.rs
@@ -6,7 +6,7 @@ use std::{
fmt::{self, Display},
fs::{self, File},
io::{self, Write},
- path::PathBuf,
+ path::{PathBuf, Path},
process::ExitCode,
sync::mpsc,
};
@@ -18,7 +18,19 @@ mod log;
mod gallery_entry;
/// A wrapper for HTML-escaped strings
-pub struct Escaped<'a>(pub &'a str);
+struct Escaped<'a>(pub &'a str);
+
+/// A wrapper to display lists of command line arguments
+struct ArgList<'a>(pub &'a [String]);
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum RenderResult {
+ Skipped,
+ Success,
+ Failure,
+}
+
+const FULL_BUILD_OPT: &str = "--full-build";
const TARGET_PATH: &str = "./site";
const PAGES_PATH: &str = "pix";
@@ -47,15 +59,18 @@ fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
- let (program, config) = match &args[..] {
- [program, config] => (program, config),
- [program] => {
- error!("Expected 1 command line argument, found none");
+ let (program, config, full_build) = match &args[..] {
+ [program, config] => (program, config, false),
+ [program, config, opt] if opt == FULL_BUILD_OPT => {
+ (program, config, true)
+ }
+ [program, _config, ..] => {
+ errorln!("Unknown arguments: {}", ArgList(&args[2..]));
usage!(program);
return ExitCode::FAILURE;
}
- [program, ..] => {
- error!("Expected 1 command line argument, found many");
+ [program] => {
+ errorln!("Expected 1 command line argument, found none");
usage!(program);
return ExitCode::FAILURE;
}
@@ -66,22 +81,22 @@ fn main() -> ExitCode {
match f.map(serde_yaml::from_reader::<_, Vec<GalleryEntry>>) {
// Error opening the config file
Err(err) => {
- error!("Couldn't open {config:?}: {err}");
+ errorln!("Couldn't open {config:?}: {err}");
usage!(program);
ExitCode::FAILURE
}
// Error parsing the config file
Ok(Err(err)) => {
- error!("Couldn't parse {config:?}: {err}");
+ errorln!("Couldn't parse {config:?}: {err}");
usage_config!();
ExitCode::FAILURE
}
- Ok(Ok(pics)) => render_gallery(pics),
+ Ok(Ok(pics)) => render_gallery(pics, full_build),
}
}
/// Coordinates the rendering of all the pages and file conversions
-fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
+fn render_gallery(pics: Vec<GalleryEntry>, full_build: bool) -> ExitCode {
info!("Copying image files to the target directory...");
for pic in &pics {
@@ -126,15 +141,15 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
let sender = sender.clone();
let pic = pic.clone();
rendering_pool.execute(move || {
- sender.send(render_thumbnail(pic))
+ sender.send(render_thumbnail(pic, full_build))
.expect("channel should still be alive awaiting for the completion of this task");
});
}
for _ in 0..pics.len() {
match reciever.recv() {
- Ok(false) => return ExitCode::FAILURE,
- Ok(true) => {}
+ Ok(RenderResult::Failure) => return ExitCode::FAILURE,
+ Ok(RenderResult::Success | RenderResult::Skipped) => {}
Err(_) => {
// Propagate the panic to the main thread: reciever.recv should
// only fail if some of the rendering threads panicked
@@ -154,10 +169,13 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
for pic in pics {
info!("Rendering HTML page for {name:?}...", name = pic.file_name);
- if render_pic_page(&pic).is_err() {
- return ExitCode::FAILURE;
+ match render_pic_page(&pic, full_build) {
+ RenderResult::Success => info_done!(),
+ RenderResult::Skipped => {
+ info_done!("Skipped! (use {FULL_BUILD_OPT} to overwrite)");
+ }
+ RenderResult::Failure => return ExitCode::FAILURE,
}
- info_done!();
}
ExitCode::SUCCESS
@@ -223,81 +241,97 @@ fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> {
writeln!(f, "</html>")
}
-fn render_pic_page(pic: &GalleryEntry) -> io::Result<()> {
+fn render_pic_page(pic: &GalleryEntry, full_build: bool) -> RenderResult {
let mut path = PathBuf::from(TARGET_PATH);
path.push(PAGES_PATH);
path.push(pic.file_name.clone() + ".html");
+ // Only try to re-render HTML page in case the page is older than the
+ // image file
+ if !full_build && !needs_update(&path, &pic.path) {
+ return RenderResult::Skipped;
+ }
+
let mut f = match File::create(&path) {
Ok(file) => file,
Err(err) => {
errorln!("Could not open file {path:?}: {err}");
- return Err(err);
+ return RenderResult::Failure;
}
};
- writeln!(f, "<!DOCTYPE html>")?;
- write_license(&mut f)?;
- writeln!(f, "<html lang=\"en\">")?;
- writeln!(f, "<head>")?;
- writeln!(
- f,
- "<title>{PAGE_TITLE} ‐ {name}</title>",
- name = Escaped(&pic.file_name)
- )?;
- write_head(&mut f)?;
- writeln!(
- f,
- "<link rel=\"preload\" as=\"image\" href=\"/{PHOTOS_PATH}/{n}\">",
- n = Escaped(&pic.file_name)
- )?;
- writeln!(f, "</head>")?;
+ /// Does the deeds
+ fn write_file(f: &mut File, pic: &GalleryEntry) -> io::Result<()> {
+ writeln!(f, "<!DOCTYPE html>")?;
+ write_license(f)?;
+ writeln!(f, "<html lang=\"en\">")?;
+ writeln!(f, "<head>")?;
+ writeln!(
+ f,
+ "<title>{PAGE_TITLE} ‐ {name}</title>",
+ name = Escaped(&pic.file_name)
+ )?;
+ write_head(f)?;
+ writeln!(
+ f,
+ "<link rel=\"preload\" as=\"image\" href=\"/{PHOTOS_PATH}/{n}\">",
+ n = Escaped(&pic.file_name)
+ )?;
+ writeln!(f, "</head>")?;
- writeln!(f, "<body>")?;
- write_nav(&mut f)?;
+ writeln!(f, "<body>")?;
+ write_nav(f)?;
- writeln!(f, "<main>")?;
- if pic.caption.is_some() {
- writeln!(f, "<figure>")?;
- } else {
- writeln!(f, "<figure aria-label=\"File {name}\">",
- name = Escaped(&pic.file_name))?;
- }
- writeln!(f, "<div id=\"picture-container\">")?;
- writeln!(
- f,
- "<img alt=\"{alt}\" src=\"/{PHOTOS_PATH}/{file_name}\">",
- alt = Escaped(&pic.alt),
- file_name = Escaped(&pic.file_name)
- )?;
- writeln!(f, "</div>")?;
- if let Some(caption) = &pic.caption {
- writeln!(f, "<figcaption>")?;
- writeln!(f, "{}", Escaped(caption))?;
- writeln!(f, "</figcaption>")?;
- }
- writeln!(f, "</figure>")?;
- writeln!(f, "</main>")?;
+ writeln!(f, "<main>")?;
+ if pic.caption.is_some() {
+ writeln!(f, "<figure>")?;
+ } else {
+ writeln!(f, "<figure aria-label=\"File {name}\">",
+ name = Escaped(&pic.file_name))?;
+ }
+ writeln!(f, "<div id=\"picture-container\">")?;
+ writeln!(
+ f,
+ "<img alt=\"{alt}\" src=\"/{PHOTOS_PATH}/{file_name}\">",
+ alt = Escaped(&pic.alt),
+ file_name = Escaped(&pic.file_name)
+ )?;
+ writeln!(f, "</div>")?;
+ if let Some(caption) = &pic.caption {
+ writeln!(f, "<figcaption>")?;
+ writeln!(f, "{}", Escaped(caption))?;
+ writeln!(f, "</figcaption>")?;
+ }
+ writeln!(f, "</figure>")?;
+ writeln!(f, "</main>")?;
+
+ writeln!(f, "<footer>")?;
+ write!(f, "original work by ")?;
+ if let Some(url) = &pic.author_url {
+ writeln!(f, "<a role=\"author\" href=\"{url}\">{author}</a>",
+ author = Escaped(&pic.author))?;
+ } else {
+ writeln!(f, "{}", Escaped(&pic.author))?;
+ }
+ writeln!(f, "<br>")?;
+ if let LicenseType::Cc(license) = &pic.license {
+ writeln!(f, "licensed under <a role=\"license\" href=\"{url}\">{license}</a>",
+ url = license.url())?;
+ } else {
+ writeln!(f, "this is public domain")?;
+ }
+ writeln!(f, "</footer>")?;
- writeln!(f, "<footer>")?;
- write!(f, "original work by ")?;
- if let Some(url) = &pic.author_url {
- writeln!(f, "<a role=\"author\" href=\"{url}\">{author}</a>",
- author = Escaped(&pic.author))?;
- } else {
- writeln!(f, "{}", Escaped(&pic.author))?;
+ writeln!(f, "</body>")?;
+ writeln!(f, "</html>")
}
- writeln!(f, "<br>")?;
- if let LicenseType::Cc(license) = &pic.license {
- writeln!(f, "licensed under <a role=\"license\" href=\"{url}\">{license}</a>",
- url = license.url())?;
+
+ if let Err(err) = write_file(&mut f, pic) {
+ errorln!("Could not write to {path:?}: {err}");
+ RenderResult::Failure
} else {
- writeln!(f, "this is public domain")?;
+ RenderResult::Success
}
- writeln!(f, "</footer>")?;
-
- writeln!(f, "</body>")?;
- writeln!(f, "</html>")
}
fn write_nav(f: &mut File) -> io::Result<()> {
@@ -363,27 +397,20 @@ fn write_license(f: &mut File) -> io::Result<()> {
)
}
-/// Returns `true` if rendering succeded (or was skipped) and `false`
-/// otherwise. All warinings and error messages should be logged in here.
// TODO: Render GIF files as mp4 instead
-fn render_thumbnail(pic: GalleryEntry) -> bool {
+fn render_thumbnail(pic: GalleryEntry, full_build: bool) -> RenderResult {
let mut thumb_path = PathBuf::from(TARGET_PATH);
thumb_path.push(THUMBS_PATH);
thumb_path.push(pic.file_name.clone() + ".webp");
// Only try to render thumbnail in case the thumbnail file in the machine
// is older than the source file
- if let (Ok(thumb_meta), Ok(img_meta)) = (fs::metadata(&thumb_path), fs::metadata(&pic.path)) {
- let thumb_mod_date = thumb_meta.modified()
- .expect("os should support file modification date");
- let img_mod_date = img_meta.modified()
- .expect("os should support file modification date");
-
- if thumb_mod_date > img_mod_date {
- warnln!("Skipped rendering the thumbnail for {name:?} (update {path:?} to overwrite)",
- name = pic.file_name, path = pic.path);
- return true;
- }
+ if !full_build && !needs_update(&thumb_path, &pic.path) {
+ warnln!(
+ "Skipped rendering the thumbnail for {name:?} (use {FULL_BUILD_OPT} to overwrite)",
+ name = pic.file_name
+ );
+ return RenderResult::Skipped;
}
let mut thumb_file = match File::create(&thumb_path) {
@@ -392,7 +419,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
errorln!(
"Couldn't open WebP thumbnail file {thumb_path:?}: {err}"
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -404,7 +431,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
path = pic.file_name,
err = err
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -415,7 +442,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
"Faileded to decode image file {name:?}: {err}",
name = pic.file_name,
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -438,10 +465,22 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
"Couldn't write WebP thumnail to file {path:?}: {err}",
path = thumb_path
);
- return false;
+ return RenderResult::Failure;
}
infoln!("Rendered WebP thumbnail for {name:?}", name = pic.file_name);
+ RenderResult::Success
+}
+
+/// Returns `false` if both `p1` and `p2` exist and and `p1` is newer than
+/// `f2`. Returns `true` otherwise
+fn needs_update<P1: AsRef<Path>, P2: AsRef<Path>>(p1: P1, p2: P2) -> bool {
+ if let (Ok(m1), Ok(m2)) = (fs::metadata(&p1), fs::metadata(&p2)) {
+ if m1.modified().unwrap() > m2.modified().unwrap() {
+ return false;
+ }
+ }
+
true
}
@@ -461,3 +500,20 @@ impl<'a> Display for Escaped<'a> {
Ok(())
}
}
+
+impl<'a> Display for ArgList<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+ let mut first = true;
+
+ for arg in self.0 {
+ if first {
+ first = false;
+ write!(f, "{:?}", arg)?;
+ } else {
+ write!(f, " {:?}", arg)?;
+ }
+ }
+
+ Ok(())
+ }
+}