]> git.r.bdr.sh - rbdr/blog/commitdiff
Generate and archive blog, allow publishing
authorRuben Beltran del Rio <redacted>
Fri, 8 Mar 2024 22:38:23 +0000 (23:38 +0100)
committerRuben Beltran del Rio <redacted>
Fri, 8 Mar 2024 22:38:23 +0000 (23:38 +0100)
15 files changed:
Cargo.toml
src/archiver/gemini.rs [new file with mode: 0644]
src/archiver/gemini.txt [deleted file]
src/archiver/gopher.rs [new file with mode: 0644]
src/archiver/gopher.txt [deleted file]
src/archiver/mod.rs
src/archiver/raw.rs [new file with mode: 0644]
src/command/generate.rs
src/command/publish.rs
src/command/publish_archive.rs
src/generator/static_files.rs
src/main.rs
src/utils.rs [new file with mode: 0644]
templates/index.gmi
templates/index.gph [new file with mode: 0644]

index 6846a682a24e2d0447ebd1d70e99b2c6be392855..fd5bceaa24eede76fad6c7a15ace76b7c53cd104 100644 (file)
@@ -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 (file)
index 0000000..8d56305
--- /dev/null
@@ -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 (file)
index e69de29..0000000
diff --git a/src/archiver/gopher.rs b/src/archiver/gopher.rs
new file mode 100644 (file)
index 0000000..820e4d1
--- /dev/null
@@ -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 (file)
index e69de29..0000000
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6f0c28483c7cbe9446bd84e4a9699addea0d58ad 100644 (file)
@@ -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<ArchiveEntry>) -> 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<String> {
+        let date = OffsetDateTime::from_unix_timestamp_nanos(
+            (self.id.parse::<u64>().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<ArchiveEntry> {
+    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<fn(&PathBuf, &PathBuf, &PathBuf, &TemplateContext) -> Result<()>> {
+    vec![
+        raw::archive,
+        gemini::archive,
+        gopher::archive
+    ]
+}
diff --git a/src/archiver/raw.rs b/src/archiver/raw.rs
new file mode 100644 (file)
index 0000000..5099f2b
--- /dev/null
@@ -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(())
+}
index c8c567392ea63ef1f7ff2ab4b12bc10337b759a0..cc9e4015dbadda0dd1d865958e29f81fa7ce7620 100644 (file)
@@ -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<Post> {
         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(())
     }
 
index 881c9876e8d6c9b590340a7bef1dc100aa179bbe..207b45ddeda0cb56d913ffa863a0ee30ad2a12a7 100644 (file)
@@ -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(())
     }
 
index f5f114aec14dfd949ec57d1e7b81ec247f2bab27..075421f0ac1b25c1f2738724f656832af6692363 100644 (file)
@@ -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(())
     }
 
index 50f56d850b4b829f4916a8ec1228e25071531ae9..401eacfa07f067db99272a4ed708456b310e3fb3 100644 (file)
@@ -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() {
index 0de896b45aa5a431b08d8733ad21971f4afd5d30..a0a9fefc61525c7b256161c64fba5693b9b17393 100644 (file)
@@ -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 (file)
index 0000000..c9d8426
--- /dev/null
@@ -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(())
+}
index 33832b18e2a48efead37fc29f242ce33323f4fb5..35bebd4677918adcb814b0042845263abb57a86d 100644 (file)
@@ -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 (file)
index 0000000..5084284
--- /dev/null
@@ -0,0 +1,4 @@
+Gemlog Archive
+
+{{~ posts: post}}
+[0|{{= post.title}}|./{{= post.id }}/{{= post.slug }}.gmi|server|port] {{~}}