X-Git-Url: https://git.r.bdr.sh/rbdr/blog/blobdiff_plain/2998247083406f914b3647cedd19abf5507bf2c6..7a40b0cf322c146439be449fc40e08a62c08ab60:/src/archiver/mod.rs?ds=sidebyside diff --git a/src/archiver/mod.rs b/src/archiver/mod.rs index e69de29..ba8dd69 100644 --- a/src/archiver/mod.rs +++ b/src/archiver/mod.rs @@ -0,0 +1,127 @@ +mod gemini; +mod gopher; +mod raw; + +use crate::template::{Context, Value}; +use std::collections::HashMap; +use std::fs::read_dir; +use std::io::Result; +use std::path::{Path, PathBuf}; +use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; + +const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); + +struct ArchiveEntry { + id: String, + slug: String, +} + +type Archiver = fn(&Path, &Path, &Path, &Context) -> Result<()>; + +impl ArchiveEntry { + pub fn to_template_context(archive_entries: &[ArchiveEntry]) -> Context { + let mut context = HashMap::new(); + + let archive_entries_collection = archive_entries + .iter() + .map(ArchiveEntry::to_template_value) + .collect(); + + context.insert( + "archive_length".to_string(), + Value::Unsigned(archive_entries.len().try_into().unwrap()), + ); + context.insert( + "posts".to_string(), + Value::Collection(archive_entries_collection), + ); + + context + } + + pub fn to_template_value(&self) -> Context { + let mut context = HashMap::new(); + + context.insert("id".to_string(), Value::String(self.id.clone())); + + context.insert("slug".to_string(), Value::String(self.slug.clone())); + + if let Some(title) = self.title() { + context.insert("title".to_string(), Value::String(title)); + } + + context + } + + fn title(&self) -> Option { + let date = OffsetDateTime::from_unix_timestamp_nanos( + (self.id.parse::().ok()? * 1_000_000).into(), + ) + .ok()?; + let short_date = date.format(&DATE_FORMAT).ok()?; + let title = self.slug.replace('-', " "); + Some(format!("{short_date} {title}")) + } +} + +fn read_archive(archive_directory: &PathBuf) -> Vec { + let mut archive_entries = Vec::new(); + if let Ok(entries) = read_dir(archive_directory) { + for entry in entries.filter_map(Result::ok) { + let entry_path = entry.path(); + let post_id = entry.file_name(); + if let Ok(entry_type) = entry.file_type() { + if entry_type.is_dir() { + if let Ok(candidates) = read_dir(&entry_path) { + for candidate in candidates.filter_map(Result::ok) { + let candidate_path = candidate.path(); + match candidate_path.extension() { + Some(extension) => { + if extension == "gmi" { + if let Some(slug) = candidate_path.file_stem() { + if let (Some(post_id), Some(slug)) = + (post_id.to_str(), slug.to_str()) + { + archive_entries.push(ArchiveEntry { + id: post_id.to_string(), + slug: slug.to_string(), + }); + } + } + } + } + _ => continue, + } + } + } + } + } + } + } + + archive_entries.sort_by(|a, b| b.id.cmp(&a.id)); + archive_entries +} + +pub fn archive( + archive_directory: &PathBuf, + template_directory: &Path, + output_directory: &Path, +) -> Result<()> { + let archivers = available_archivers(); + let archive_entries = read_archive(archive_directory); + let context = ArchiveEntry::to_template_context(&archive_entries); + for archiver in archivers { + archiver( + archive_directory, + template_directory, + output_directory, + &context, + )?; + } + Ok(()) +} + +fn available_archivers() -> Vec { + vec![raw::archive, gemini::archive, gopher::archive] +}