From: Ruben Beltran del Rio Date: Fri, 8 Mar 2024 22:38:23 +0000 (+0100) Subject: Generate and archive blog, allow publishing X-Git-Tag: 7.0.0~42 X-Git-Url: https://git.r.bdr.sh/rbdr/blog/commitdiff_plain/6352ebb0eb4cb83240c6d4998e0ef1375b041191?ds=inline Generate and archive blog, allow publishing --- diff --git a/Cargo.toml b/Cargo.toml index 6846a68..fd5bcea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,6 @@ version = "7.0.0" edition = "2021" [dependencies] -time = { version = "0.3.34", features = ["formatting"] } +time = { version = "0.3.34", features = ["macros", "formatting"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" diff --git a/src/archiver/gemini.rs b/src/archiver/gemini.rs new file mode 100644 index 0000000..8d56305 --- /dev/null +++ b/src/archiver/gemini.rs @@ -0,0 +1,19 @@ +use std::fs::write; +use std::io::Result; +use std::path::PathBuf; +use crate::template::{find, parse, TemplateContext}; + +const FILENAME: &str = "index.gmi"; + +pub fn archive(_: &PathBuf, template_directory: &PathBuf, target: &PathBuf, context: &TemplateContext) -> Result<()> { + match find(template_directory, FILENAME) { + Some(template) => { + let parsed_template = parse(&template); + let rendered_template = parsed_template.render(context); + let location = target.join(FILENAME); + write(location, rendered_template)?; + }, + None => {} + } + Ok(()) +} diff --git a/src/archiver/gemini.txt b/src/archiver/gemini.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/archiver/gopher.rs b/src/archiver/gopher.rs new file mode 100644 index 0000000..820e4d1 --- /dev/null +++ b/src/archiver/gopher.rs @@ -0,0 +1,19 @@ +use std::fs::write; +use std::io::Result; +use std::path::PathBuf; +use crate::template::{find, parse, TemplateContext}; + +const FILENAME: &str = "index.gph"; + +pub fn archive(_: &PathBuf, template_directory: &PathBuf, target: &PathBuf, context: &TemplateContext) -> Result<()> { + match find(template_directory, FILENAME) { + Some(template) => { + let parsed_template = parse(&template); + let rendered_template = parsed_template.render(context); + let location = target.join(FILENAME); + write(location, rendered_template)?; + }, + None => {} + } + Ok(()) +} diff --git a/src/archiver/gopher.txt b/src/archiver/gopher.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/archiver/mod.rs b/src/archiver/mod.rs index e69de29..6f0c284 100644 --- a/src/archiver/mod.rs +++ b/src/archiver/mod.rs @@ -0,0 +1,130 @@ +mod raw; +mod gemini; +mod gopher; + +use std::collections::HashMap; +use std::fs::read_dir; +use std::io::Result; +use std::path::PathBuf; +use time::{OffsetDateTime, format_description::FormatItem, macros::format_description}; +use crate::template::{TemplateContext, TemplateValue}; + +const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); + +struct ArchiveEntry { + id: String, + slug: String +} + +impl ArchiveEntry { + pub fn to_template_context(archive_entries: &Vec) -> TemplateContext { + let mut context = HashMap::new(); + + let archive_entries_collection = archive_entries + .iter() + .map(|archive_entry| archive_entry.to_template_value()) + .collect(); + + context.insert( + "archive_length".to_string(), + TemplateValue::Unsigned( + archive_entries.len().try_into().unwrap() + ) + ); + context.insert( + "posts".to_string(), + TemplateValue::Collection(archive_entries_collection) + ); + + context + } + + pub fn to_template_value(&self) -> TemplateContext { + let mut context = HashMap::new(); + + context.insert( + "id".to_string(), + TemplateValue::String(self.id.clone()) + ); + + context.insert( + "slug".to_string(), + TemplateValue::String(self.slug.clone()) + ); + + if let Some(title) = self.title() { + context.insert( + "title".to_string(), + TemplateValue::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: &PathBuf, output_directory: &PathBuf) -> 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)?; + } + return Ok(()) +} + +fn available_archivers() -> Vec Result<()>> { + vec![ + raw::archive, + gemini::archive, + gopher::archive + ] +} diff --git a/src/archiver/raw.rs b/src/archiver/raw.rs new file mode 100644 index 0000000..5099f2b --- /dev/null +++ b/src/archiver/raw.rs @@ -0,0 +1,11 @@ +use std::io::Result; +use std::path::PathBuf; +use crate::template::TemplateContext; +use crate::utils::recursively_copy; + +pub fn archive(archive_directory: &PathBuf, _: &PathBuf, target: &PathBuf, _: &TemplateContext) -> Result<()> { + if archive_directory.exists() { + return recursively_copy(archive_directory, target); + } + Ok(()) +} diff --git a/src/command/generate.rs b/src/command/generate.rs index c8c5673..cc9e401 100644 --- a/src/command/generate.rs +++ b/src/command/generate.rs @@ -5,6 +5,7 @@ use crate::configuration::Configuration; use crate::constants::METADATA_FILENAME; use crate::gemini_parser::parse; use crate::generator::generate; +use crate::archiver::archive; use crate::metadata::Metadata; use crate::post::Post; @@ -18,7 +19,7 @@ impl Generate { fn read_posts(&self, posts_directory: &PathBuf, max_posts: u8) -> Vec { let mut posts = Vec::new(); - for i in 0..max_posts - 1 { + for i in 0..max_posts { let post_directory = posts_directory.join(i.to_string()); match self.read_post(&post_directory, i) { Some(post) => posts.push(post), @@ -82,6 +83,11 @@ impl super::Command for Generate { let _ = remove_dir_all(&configuration.archive_output_directory); create_dir_all(&configuration.archive_output_directory)?; + archive( + &configuration.archive_directory, + &configuration.templates_directory, + &configuration.archive_output_directory + )?; return Ok(()) } diff --git a/src/command/publish.rs b/src/command/publish.rs index 881c987..207b45d 100644 --- a/src/command/publish.rs +++ b/src/command/publish.rs @@ -1,5 +1,8 @@ use std::io::Result; use crate::configuration::Configuration; +use std::process::{Command, Stdio}; + +const COMMAND: &str = "rsync"; pub struct Publish; @@ -14,8 +17,26 @@ impl super::Command for Publish { vec![] } - fn execute(&self, input: Option<&String>, _: &Configuration, _: &String) -> Result<()> { - println!("Publish: {:?}!", input); + fn execute(&self, input: Option<&String>, configuration: &Configuration, _: &String) -> Result<()> { + + let input = input.expect("You must provide a location to publish the blog"); + + Command::new(COMMAND) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("Publishing requires rsync"); + + + Command::new(COMMAND) + .arg("-r") + .arg(format!("{}/", &configuration.blog_output_directory.display())) + .arg(input.as_str()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("Publishing requires rsync"); return Ok(()) } diff --git a/src/command/publish_archive.rs b/src/command/publish_archive.rs index f5f114a..075421f 100644 --- a/src/command/publish_archive.rs +++ b/src/command/publish_archive.rs @@ -1,5 +1,8 @@ use std::io::Result; use crate::configuration::Configuration; +use std::process::{Command, Stdio}; + +const COMMAND: &str = "rsync"; pub struct PublishArchive; @@ -14,8 +17,26 @@ impl super::Command for PublishArchive { vec![] } - fn execute(&self, input: Option<&String>, _: &Configuration, _: &String) -> Result<()> { - println!("Publish Archive: {:?}!", input); + fn execute(&self, input: Option<&String>, configuration: &Configuration, _: &String) -> Result<()> { + + let input = input.expect("You must provide a location to publish the archive"); + + Command::new(COMMAND) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("Publishing requires rsync"); + + + Command::new(COMMAND) + .arg("-r") + .arg(format!("{}/", &configuration.archive_output_directory.display())) + .arg(input.as_str()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("Publishing requires rsync"); return Ok(()) } diff --git a/src/generator/static_files.rs b/src/generator/static_files.rs index 50f56d8..401eacf 100644 --- a/src/generator/static_files.rs +++ b/src/generator/static_files.rs @@ -1,27 +1,7 @@ -use std::fs::{copy, create_dir_all, read_dir}; use std::io::Result; use std::path::PathBuf; use crate::template::TemplateContext; - -fn recursively_copy(source: &PathBuf, target: &PathBuf) -> Result<()> { - let entries = read_dir(source)?; - for entry in entries { - let entry = entry?; - let entry_type = entry.file_type()?; - let entry_name = entry.file_name(); - let entry_source = entry.path(); - let entry_target = target.join(entry_name); - - if entry_type.is_dir() { - create_dir_all(&entry_target)?; - recursively_copy(&entry_source, &entry_target)?; - } else { - copy(&entry_source, &entry_target)?; - } - } - - Ok(()) -} +use crate::utils::recursively_copy; pub fn generate(source: &PathBuf, _: &PathBuf, target: &PathBuf, _: &TemplateContext) -> Result<()> { if source.exists() { diff --git a/src/main.rs b/src/main.rs index 0de896b..a0a9fef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ mod command; mod constants; mod gemini_parser; mod generator; +mod archiver; mod metadata; mod post; mod template; +mod utils; use std::iter::once; use std::env::args; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c9d8426 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,23 @@ +use std::io::Result; +use std::path::PathBuf; +use std::fs::{copy, create_dir_all, read_dir}; + +pub fn recursively_copy(source: &PathBuf, target: &PathBuf) -> Result<()> { + let entries = read_dir(source)?; + for entry in entries { + let entry = entry?; + let entry_type = entry.file_type()?; + let entry_name = entry.file_name(); + let entry_source = entry.path(); + let entry_target = target.join(entry_name); + + if entry_type.is_dir() { + create_dir_all(&entry_target)?; + recursively_copy(&entry_source, &entry_target)?; + } else { + copy(&entry_source, &entry_target)?; + } + } + + Ok(()) +} diff --git a/templates/index.gmi b/templates/index.gmi index 33832b1..35bebd4 100644 --- a/templates/index.gmi +++ b/templates/index.gmi @@ -1,3 +1,4 @@ # Gemlog Archive -{{= posts }} +{{~ posts: post}} +=> ./{{= post.id }}/{{= post.slug }}.gmi {{= post.title}} {{~}} diff --git a/templates/index.gph b/templates/index.gph new file mode 100644 index 0000000..5084284 --- /dev/null +++ b/templates/index.gph @@ -0,0 +1,4 @@ +Gemlog Archive + +{{~ posts: post}} +[0|{{= post.title}}|./{{= post.id }}/{{= post.slug }}.gmi|server|port] {{~}}