Write out individual files for each post

This commit is contained in:
Wesley Moore 2024-11-24 16:17:00 +10:00
parent d66f330fd1
commit f339cd76f2
No known key found for this signature in database
4 changed files with 88 additions and 23 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/target /target
/archive /archive
/archive.html /public

View file

@ -1,11 +1,11 @@
use jiff::tz::TimeZone; use jiff::tz::TimeZone;
use jiff::Timestamp; use jiff::Timestamp;
use pleroma::Activities; use pleroma::Activities;
use serde::{Deserialize, Serialize}; use std::ffi::OsStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::{ use std::{
collections::HashMap, collections::HashMap,
env, fmt, env, fs,
fs::File, fs::File,
io::{self, BufReader, BufWriter}, io::{self, BufReader, BufWriter},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -18,19 +18,25 @@ mod pleroma;
type BoxError = Box<dyn std::error::Error>; type BoxError = Box<dyn std::error::Error>;
type Mappings = HashMap<String, Option<Url>>; type Mappings = HashMap<String, Option<Url>>;
const STYLE: &str = include_str!("../style.css");
static TZ: OnceLock<TimeZone> = OnceLock::new(); static TZ: OnceLock<TimeZone> = OnceLock::new();
static MAPPINGS: OnceLock<Mappings> = OnceLock::new(); static MAPPINGS: OnceLock<Mappings> = OnceLock::new();
fn main() -> ExitCode { fn main() -> ExitCode {
let Some(path) = env::args_os().skip(1).next().map(PathBuf::from) else { let args = env::args_os().skip(1).collect::<Vec<_>>();
eprintln!("Usage: pleroma-archive path/to/pleroma-archive"); let (archive_path, output_path) = match args.as_slice() {
return ExitCode::FAILURE; [archive, output] => (Path::new(archive), Path::new(output)),
_ => {
eprintln!("Usage: pleroma-archive path/to/pleroma-archive output/path");
return ExitCode::FAILURE;
}
}; };
let timezone = TimeZone::system(); let timezone = TimeZone::system();
TZ.set(timezone).unwrap(); TZ.set(timezone).unwrap();
match try_main(&path) { match try_main(archive_path, output_path) {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(err) => { Err(err) => {
eprintln!("Error: {err}"); eprintln!("Error: {err}");
@ -39,7 +45,7 @@ fn main() -> ExitCode {
} }
} }
fn try_main(path: &Path) -> Result<(), BoxError> { fn try_main(path: &Path, output_path: &Path) -> Result<(), BoxError> {
let actor_path = path.join("actor.json"); let actor_path = path.join("actor.json");
let outbox_path = path.join("outbox.json"); let outbox_path = path.join("outbox.json");
let mappings_path = path.join("mappings.json"); let mappings_path = path.join("mappings.json");
@ -57,8 +63,14 @@ fn try_main(path: &Path) -> Result<(), BoxError> {
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
// Ensure output path exists, and write out stylesheet
fs::create_dir_all(output_path)?;
let style_path = output_path.join("style.css");
fs::write(&style_path, STYLE)?;
let agent = ureq::AgentBuilder::new().redirects(0).build(); let agent = ureq::AgentBuilder::new().redirects(0).build();
// Process posts
let mut posts = Vec::with_capacity(activities.ordered_items.len()); let mut posts = Vec::with_capacity(activities.ordered_items.len());
for item in &activities.ordered_items { for item in &activities.ordered_items {
if item.direct_message { if item.direct_message {
@ -109,42 +121,91 @@ fn try_main(path: &Path) -> Result<(), BoxError> {
MAPPINGS.set(mappings).unwrap(); MAPPINGS.set(mappings).unwrap();
// dbg!(&posts); // Generate index.html
let index_html = Layout {
println!( title: "Pleroma Archive",
"{}", body: Index {
Page {
title: "Pleroma Archive",
actor: &actor, actor: &actor,
activities: &posts, activities: &posts,
},
actor: &actor,
}
.to_string();
let index_path = output_path.join("index.html");
println!("Writing {}", index_path.display());
fs::write(&index_path, index_html.as_bytes())?;
// Generate individual post pages
for post in posts {
let Some(Some(url)) = MAPPINGS.get().unwrap().get(&post.id) else {
continue;
};
let mut post_path = output_path
.iter()
.chain(
url.path_segments()
.ok_or_else(|| BoxError::from("unable to get path segments of {url}"))?
.map(OsStr::new),
)
.collect::<PathBuf>();
post_path.set_extension("html");
let post_html = Layout {
title: &format!(
"Post from {} on {}",
actor.preferred_username,
post.human_published()
),
body: Show {
actor: &actor,
activity: post,
},
actor: &actor,
} }
); .to_string();
println!("Writing {}", post_path.display());
fs::create_dir_all(&post_path.parent().expect("post has parent dir"))?;
fs::write(&post_path, post_html.as_bytes())?;
}
Ok(()) Ok(())
} }
markup::define! { markup::define! {
Page<'a>(title: &'a str, actor: &'a pleroma::Actor, activities: &'a [&'a pleroma::Activity]) { Layout<'a, Body: markup::Render>(title: &'a str, body: Body, actor: &'a pleroma::Actor) {
@markup::doctype() @markup::doctype()
html { html {
head { head {
meta[charset="utf-8"]; meta[charset="utf-8"];
meta[name="viewport", content="width=device-width, initial-scale=1"]; meta[name="viewport", content="width=device-width, initial-scale=1"];
title { @title } title { @title }
link[rel="stylesheet", type="text/css", href="style.css"]; link[rel="stylesheet", type="text/css", href="/style.css"];
} }
body { body {
@Header { title, actor } @Header { title, actor }
main { @body
@for activity in activities.iter().rev() {
@Activity { actor, activity }
}
}
@Footer { } @Footer { }
} }
} }
} }
Index<'a>(actor: &'a pleroma::Actor, activities: &'a [&'a pleroma::Activity]) {
main {
p { @activities.len() " posts:" }
@for activity in activities.iter().rev() {
@Activity { actor, activity }
}
}
}
Show<'a>(actor: &'a pleroma::Actor, activity: &'a pleroma::Activity) {
a[href="/"] { "☜ Back to home page" }
@Activity { actor, activity }
}
Header<'a>(title: &'a str, actor: &'a pleroma::Actor) { Header<'a>(title: &'a str, actor: &'a pleroma::Actor) {
header { header {
h1 { @title } h1 { @title }
@ -165,7 +226,7 @@ markup::define! {
@Actor { actor } @Actor { actor }
div[class="activity-content"] { div[class="activity-content"] {
a[href=MAPPINGS.get().unwrap().get(&activity.id).and_then(|url| url.as_ref().map(|url| url.as_str()))] { a[href=MAPPINGS.get().unwrap().get(&activity.id).and_then(|url| url.as_ref().map(|url| url.path()))] {
time[datetime=&activity.published] { @activity.human_published() } time[datetime=&activity.published] { @activity.human_published() }
} }

View file

@ -1,3 +1,5 @@
#![allow(unused)]
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]

View file

@ -1,3 +1,5 @@
#![allow(unused)]
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]