a repo's og cards are a set named by what they're drawn from, which a generation links to, so a write whose repos didn't move plans and hashes none of them
migration: the cards' keys now take the fonts' digest, so the first generate after the deploy draws every card again (about 15 s on prod's 94 repos, inside the source locks), once. the old og-cards/*.png that no set names go after the hour's grace. Signed-off-by: dawn <dawn@klbr.net>
619 unmodified linesExpand all620621622623624624625626627628629630631632633634132 unmodified linesExpand all619 unmodified linesExpand all served: source.served.clone(), seal: &sealed.key, }) .collect(); let cards = crate::og_card::plan(config.hostname.as_str(), card_repos, card_users, &card_git)?; let cards = crate::og_card::plan( &output_root, config.hostname.as_str(), card_repos, card_users, &card_git, )?; crate::og_card::emit(&output_root, &build_dir, cards).context("emit og cards")?; // a blob write waits on the sweep. the source locks drop right after it drop(_source_locks); // chain heads are generation identity, since a ref flipped back still moves the rev132 unmodified linesExpand all30 unmodified lines3132333435363738394041424344454638474849415051434445464752535455565758596061626364656667686970301 unmodified linesExpand all30 unmodified lines mono_bold: fontdue::Font, wide: fontdue::Font,}
/// the faces' files, in `Fonts` orderconst FONT_FILES: [&[u8]; 5] = [ include_bytes!("fonts/host-grotesk-400.ttf"), include_bytes!("fonts/host-grotesk-600.ttf"), include_bytes!("fonts/kode-mono-400.ttf"), include_bytes!("fonts/kode-mono-700.ttf"), include_bytes!("../favicon/JetBrainsMonoNL-Medium.ttf"),];
fn fonts() -> &'static Fonts { static FONTS: OnceLock<Fonts> = OnceLock::new(); FONTS.get_or_init(|| { let load = |bytes: &'static [u8]| { let [sans, sans_bold, mono, mono_bold, wide] = FONT_FILES.map(|bytes| { fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default()) .expect("the card fonts are built in") }; }); Fonts { sans: load(include_bytes!("fonts/host-grotesk-400.ttf")), sans_bold: load(include_bytes!("fonts/host-grotesk-600.ttf")), mono: load(include_bytes!("fonts/kode-mono-400.ttf")), mono_bold: load(include_bytes!("fonts/kode-mono-700.ttf")), wide: load(include_bytes!("../favicon/JetBrainsMonoNL-Medium.ttf")), sans, sans_bold, mono, mono_bold, wide, } })}
/// the fonts' hash, so new font files draw every card againpub(crate) fn fonts_digest() -> &'static str { static DIGEST: OnceLock<String> = OnceLock::new(); DIGEST.get_or_init(|| crate::atproto::short_hash(&FONT_FILES.concat()))}
impl Face { fn font(self) -> &'static fontdue::Font { let fonts = fonts(); match self {301 unmodified linesExpand all123423456789101112131411 unmodified lines2627282930313233343536373839108 unmodified linesExpand all1481491501511431521531541551561571581591601611621631641651661671681691701711 unmodified line173174175176153177178179180181182183184230 unmodified linesExpand all4154164174183923933943953963973983994194204214224014234244254264274284294304054064074084094104114124134144154164174184194204214224314324334344354364374384394404414424434444454234464474264274284294304314324334484494504514524534544554564354364374384394574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974414424984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915924485935945955965975982 unmodified lines//! open graph cards for a knot's pages: a repo's, its tickets', the commits svfe renders and//! its owners'. each is drawn once for what it shows and kept by that in the output root, a//! generation links the ones it names under `www/og`, and nginx serves the repo's card, then the//! knot's, for a page without its own//! its owners'. each is drawn once for what it shows and kept by that in the output root. a//! repo's cards are linked into a set named by what they're drawn from, a generation links each//! repo's set under `www/og`, and nginx serves the repo's card, then the knot's, for a page//! without its own
use std::collections::BTreeSet;use std::path::{Path, PathBuf};use std::sync::atomic::{AtomicUsize, Ordering};use std::time::Duration;
use anyhow::Context as _;use knot_types::Oid;use rayon::prelude::*;11 unmodified linespub(crate) use plan::{GitRepo, SocialPull, SocialRepo, SocialTicket, SocialUser, plan};
/// the drawn cards, by the hash of what they showpub(crate) const CARDS_DIR: &str = "og-cards";/// the repos' sets, by the hash of what their cards are drawn fromconst SETS_DIR: &str = "sets";/// a set's repo cardconst REPO_CARD: &str = "repo.png";/// how long a set or card nothing names is kept, for a generation still being builtconst GRACE: Duration = Duration::from_secs(3600);/// where a generation serves thempub(crate) const WWW_DIR: &str = "og";/// the commit pages svfe-pages keeps of a repo, newest first in the log's orderpub(crate) const COMMITS: usize = 60;108 unmodified linesExpand all file: Option<PathBuf>, },}
/// a card a generation serves at `www/og/<path>`/// the cards a generation serves: each repo's set, and its owners'pub(crate) struct Plan { pub(crate) sets: Vec<Set>, pub(crate) users: Vec<Planned>,}
/// a repo's cards, under a name that settles every byte of them, so a set is never written/// again: other inputs make another setpub(crate) struct Set { pub(crate) did: String, pub(crate) name: String, /// what to draw, at paths in the set. none when the set is there already pub(crate) cards: Vec<Planned>,}
/// a card at `path` in its set, or an owner's at `www/og/<path>`pub(crate) struct Planned { pub(crate) path: String, pub(crate) key: String, pub(crate) card: Lazy,1 unmodified line
/// what keys a card: everything it shows, or what that is read frompub(crate) fn key(shown: &impl Serialize) -> anyhow::Result<String> { Ok(crate::atproto::short_hash(&serde_json::to_vec(&( VERSION, DARK, shown, VERSION, DARK, canvas::fonts_digest(), shown, ))?))}
pub(crate) struct Theme {230 unmodified linesExpand all .map(|at| at.format("%Y-%m-%d").to_string()) .unwrap_or_default()}
/// links each planned card into `build_dir`, drawing the ones no earlier generation drew, and/// drops the drawn cards nothing names any more. a card that can't be read or drawn is left/// out, so its page shows the repo's cardpub(crate) fn emit( output_root: &Path, build_dir: &Path, planned: Vec<Planned>,) -> anyhow::Result<()> {/// draws what's missing, links each repo's set and the owners' cards into `build_dir`, and/// sweeps what neither this generation nor a kept one namespub(crate) fn emit(output_root: &Path, build_dir: &Path, plan: Plan) -> anyhow::Result<()> { let cards = output_root.join(CARDS_DIR); std::fs::create_dir_all(&cards).with_context(|| format!("create {}", cards.display()))?; let sets = cards.join(SETS_DIR); std::fs::create_dir_all(&sets).with_context(|| format!("create {}", sets.display()))?; // a generation's links outlive the path it was built at let sets = std::fs::canonicalize(&sets)?; let www = build_dir.join("www").join(WWW_DIR); std::fs::create_dir_all(&www).with_context(|| format!("create {}", www.display()))?; let theme = theme(); let started = std::time::Instant::now(); let planned_count = planned.len(); let drawn = std::sync::atomic::AtomicUsize::new(0); let kept: Vec<Option<String>> = planned .into_par_iter() .map(|plan| { let file = cards.join(format!("{}.png", plan.key)); if !file.is_file() { drawn.fetch_add(1, std::sync::atomic::Ordering::Relaxed); match plan .card .resolve() .and_then(|card| draw::draw(&card, &theme)) { Ok(png) => atomic_write(&file, &png) .with_context(|| format!("write {}", file.display()))?, Err(error) => { tracing::warn!(path = %plan.path, "no og card: {error:#}"); return Ok(None); let drawn = AtomicUsize::new(0); let card = |plan: Planned| -> anyhow::Result<Option<(String, PathBuf)>> { let file = cards.join(format!("{}.png", plan.key)); if !file.is_file() { drawn.fetch_add(1, Ordering::Relaxed); match plan .card .resolve() .and_then(|card| draw::draw(&card, &theme)) { Ok(png) => atomic_write(&file, &png) .with_context(|| format!("write {}", file.display()))?, Err(error) => { tracing::warn!(path = %plan.path, "no og card: {error:#}"); return Ok(None); } } } let target = www.join(&plan.path); if let Some(parent) = target.parent() { std::fs::create_dir_all(parent)?; } link_or_reflink(&file, &target) .with_context(|| format!("link {}", target.display()))?; Ok(Some(plan.key)) }) } Ok(Some((plan.path, file))) };
let users: Vec<_> = plan .users .into_par_iter() .map(card) .collect::<anyhow::Result<_>>()?; let kept: BTreeSet<String> = kept .into_iter() .flatten() .map(|key| format!("{key}.png")) .collect(); let mut owners = BTreeSet::new(); for (path, file) in users.into_iter().flatten() { let target = www.join(&path); link_or_reflink(&file, &target).with_context(|| format!("link {}", target.display()))?; owners.insert(file); }
let mut wanted = Vec::new(); let mut work = Vec::new(); for (index, set) in plan.sets.into_iter().enumerate() { wanted.push((set.did, set.name, !set.cards.is_empty())); work.extend(set.cards.into_iter().map(|planned| (index, planned))); } let done: Vec<(usize, Option<(String, PathBuf)>)> = work .into_par_iter() .map(|(index, planned)| Ok((index, card(planned)?))) .collect::<anyhow::Result<_>>()?; let mut contents = vec![Vec::new(); wanted.len()]; for (index, done) in done { contents[index].push(done); } let mut built = 0; let mut named = BTreeSet::new(); for ((did, name, fresh), contents) in wanted.iter().zip(contents) { let dir = sets.join(name); if *fresh && !dir.is_dir() { match contents.into_iter().collect::<Option<Vec<_>>>() { Some(contents) => build_set(&sets, name, &contents)?, // a card it can't draw now leaves the repo to the knot's card until a later write None => continue, } built += 1; } let target = www.join(did); std::os::unix::fs::symlink(&dir, &target) .with_context(|| format!("link {}", target.display()))?; std::os::unix::fs::symlink(dir.join(REPO_CARD), www.join(format!("{did}.png")))?; named.insert(name.clone()); } let drawn = drawn.into_inner(); tracing::info!( cards = planned_count, drawn = drawn.into_inner(), sets = wanted.len(), built, drawn, seconds = started.elapsed().as_secs_f64(), "og cards" ); // what a sweep leaves only costs disk, so it doesn't fail the write if (drawn > 0 || built > 0) && let Err(error) = sweep(output_root, &named, &owners, GRACE) { tracing::warn!("og card sweep: {error:#}"); } Ok(())}
/// links a set's cards into a staging directory and renames it into place wholefn build_set(sets: &Path, name: &str, contents: &[(String, PathBuf)]) -> anyhow::Result<()> { let staging = tempfile::Builder::new() .prefix(".staging-") .tempdir_in(sets)?; for (path, file) in contents { let target = staging.path().join(path); link_or_reflink(file, &target).with_context(|| format!("link {}", target.display()))?; } crate::cas::seal_read_only(staging.path())?; let dir = sets.join(name); match std::fs::rename(staging.path(), &dir) { Ok(()) => { // it's the set now, not staging to clean up let _ = staging.keep(); Ok(()) } // a write alongside built the same set Err(_) if dir.is_dir() => Ok(()), Err(error) => Err(error).with_context(|| format!("publish {}", dir.display())), }}
/// drops the sets that neither `named` nor a kept generation names, and the drawn cards no kept/// set or owner links, once they're older than `grace`pub(super) fn sweep( output_root: &Path, named: &BTreeSet<String>, owners: &BTreeSet<PathBuf>, grace: Duration,) -> anyhow::Result<()> { use std::os::unix::fs::MetadataExt as _; let cards = output_root.join(CARDS_DIR); let sets = cards.join(SETS_DIR); let mut kept = named.clone(); let generations = output_root.join("generations"); for generation in std::fs::read_dir(&generations).into_iter().flatten() { let www = generation?.path().join("www").join(WWW_DIR); for entry in std::fs::read_dir(&www).into_iter().flatten() { if let Ok(target) = std::fs::read_link(entry?.path()) && let Some(name) = target.file_name() && target.parent().and_then(Path::file_name) == Some(SETS_DIR.as_ref()) { kept.insert(name.to_string_lossy().into_owned()); } } } let old = |path: &Path| { std::fs::symlink_metadata(path) .and_then(|meta| meta.modified()) .is_ok_and(|at| at.elapsed().is_ok_and(|age| age >= grace)) }; let mut linked = BTreeSet::new(); for entry in std::fs::read_dir(&sets)? { let path = entry?.path(); let name = path.file_name().unwrap_or_default().to_string_lossy(); if kept.contains(name.as_ref()) { let mut dirs = vec![path]; while let Some(dir) = dirs.pop() { for entry in std::fs::read_dir(&dir)? { let entry = entry?; let meta = entry.metadata()?; match meta.is_dir() { true => dirs.push(entry.path()), false => { linked.insert((meta.dev(), meta.ino())); } } } } } else if old(&path) { std::fs::remove_dir_all(&path).with_context(|| format!("remove {}", path.display()))?; } } for owner in owners { let meta = std::fs::metadata(owner)?; linked.insert((meta.dev(), meta.ino())); } for entry in std::fs::read_dir(&cards)? { let entry = entry?; if !kept.contains(entry.file_name().to_string_lossy().as_ref()) { let meta = entry.metadata()?; if meta.is_file() && !linked.contains(&(meta.dev(), meta.ino())) && old(&entry.path()) { std::fs::remove_file(entry.path()) .with_context(|| format!("remove {}", entry.path().display()))?; } }2 unmodified lines12123456788910111213141516174 unmodified lines2223242526272829304 unmodified lines35363738394041424316 unmodified lines6061626364656667686970716566727374756869707176737475767778798081828384858687888977787980818283919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125848586878889902 unmodified lines9394959613797989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317415017517617717813 unmodified lines19219319419517219619719819920024 unmodified lines22522622722820522923023123223333 unmodified lines//! the cards a generation serves: one per repo, ticket and pull, one per commit svfe renders,//! and one per owner//! the cards a generation serves: a set per repo, with its card, one per ticket and pull and//! one per commit svfe renders, and one per owner
use std::path::Path;
use knot_types::Oid;use serde::Serialize;
use super::{COMMITS, Card, Lazy, Planned, Ribbon, key};use super::{CARDS_DIR, COMMITS, Card, Lazy, Plan, Planned, REPO_CARD, Ribbon, SETS_DIR, Set, key};use crate::store::RepoStore;
/// a hosted repo's records, as the social reads show them#[derive(Serialize)]pub(crate) struct SocialRepo { pub(crate) did: String, pub(crate) owner_did: String, pub(crate) owner_handle: String,4 unmodified lines pub(crate) open_pulls: usize, pub(crate) tickets: Vec<SocialTicket>,}
#[derive(Serialize)]pub(crate) struct SocialTicket { pub(crate) rkey: String, pub(crate) title: String, /// open, closed, completed or merged4 unmodified lines pub(crate) body: String, pub(crate) pull: Option<SocialPull>,}
#[derive(Serialize)]pub(crate) struct SocialPull { pub(crate) from: String, pub(crate) into: String, /// the newest round's range16 unmodified lines pub(crate) served: RepoStore, pub(crate) seal: &'a str,}
/// a set for each repo, drawn only when no earlier write left one under its name, and the/// owners' cardspub(crate) fn plan( output_root: &Path, host: &str, repos: Vec<SocialRepo>, users: Vec<SocialUser>, git: &[GitRepo],) -> anyhow::Result<Vec<Planned>> { let mut planned = Vec::new();) -> anyhow::Result<Plan> { let sets_dir = output_root.join(CARDS_DIR).join(SETS_DIR); let mut sets = Vec::new(); for repo in repos { let ribbon = Ribbon { host: host.to_string(), path: format!("{} / {}", repo.owner_handle, repo.name), }; let git = git.iter().find(|git| git.did == repo.did); let Log { branch, tip, commits, } = git.map(log).unwrap_or_default(); let card = Card::Repo { ribbon: Ribbon { host: host.to_string(), path: repo.owner_handle.clone(), }, name: repo.name.clone(), description: repo.description.clone(), topics: repo.topics.clone(), open_tickets: repo.open_tickets, open_pulls: repo.open_pulls, branch: branch.clone(), languages: Vec::new(), // everything the cards show is read from these, the git through the seal's refs let name = key(&(host, &repo, git.map(|git| git.seal)))?; let did = repo.did.clone(); let cards = match sets_dir.join(&name).is_dir() { true => Vec::new(), false => repo_cards(host, repo, git)?, }; let tip = tip.map(|(commit, tree)| (commit.to_hex(), tree.to_hex())); // the languages are read from the tree, so a push that keeps it keeps the card let repo_key = key(&(&card, tip.as_ref().map(|(_, tree)| tree)))?; let lazy = match (git, tip) { (Some(git), Some((commit, _))) => Lazy::Languages { card, served: git.served.clone(), seal: git.seal.to_string(), commit, }, _ => Lazy::Ready(card), }; planned.push(Planned { path: format!("{}.png", repo.did), key: repo_key, card: lazy, }); for ticket in repo.tickets { planned.push(ticket_card(&repo.did, &ribbon, ticket, git)?); } let Some(git) = git else { continue; }; for oid in commits { planned.push(Planned { path: format!("{}/commit/{}.png", repo.did, oid.to_hex()), key: key(&(&ribbon, &branch, oid.to_hex()))?, card: Lazy::Commit { export: git.export.to_path_buf(), oid, ribbon: ribbon.clone(), branch: branch.clone(), }, }); } sets.push(Set { did, name, cards }); } let mut owners = Vec::new(); for user in users { let card = Card::User { host: host.to_string(), handle: format!("@{}", user.handle),2 unmodified lines repos: user.repos, avatar: None, }; let key = key(&(&card, user.avatar.as_ref().map(|(_, cid)| cid)))?; planned.push(Planned { owners.push(Planned { path: format!("{}.png", user.did), key, card: Lazy::Avatar { card, file: user.avatar.map(|(file, _)| file), }, }); } Ok(Plan { sets, users: owners, })}
/// a repo's cards at their paths in its setfn repo_cards(host: &str, repo: SocialRepo, git: Option<&GitRepo>) -> anyhow::Result<Vec<Planned>> { let ribbon = Ribbon { host: host.to_string(), path: format!("{} / {}", repo.owner_handle, repo.name), }; let Log { branch, tip, commits, } = git.map(log).unwrap_or_default(); let card = Card::Repo { ribbon: Ribbon { host: host.to_string(), path: repo.owner_handle.clone(), }, name: repo.name.clone(), description: repo.description.clone(), topics: repo.topics.clone(), open_tickets: repo.open_tickets, open_pulls: repo.open_pulls, branch: branch.clone(), languages: Vec::new(), }; let tip = tip.map(|(commit, tree)| (commit.to_hex(), tree.to_hex())); // the languages are read from the tree, so a push that keeps it keeps the card let repo_key = key(&(&card, tip.as_ref().map(|(_, tree)| tree)))?; let lazy = match (git, tip) { (Some(git), Some((commit, _))) => Lazy::Languages { card, served: git.served.clone(), seal: git.seal.to_string(), commit, }, _ => Lazy::Ready(card), }; let mut planned = vec![Planned { path: REPO_CARD.to_string(), key: repo_key, card: lazy, }]; for ticket in repo.tickets { planned.push(ticket_card(&ribbon, ticket, git)?); } let Some(git) = git else { return Ok(planned); }; for oid in commits { planned.push(Planned { path: format!("commit/{}.png", oid.to_hex()), key: key(&(&ribbon, &branch, oid.to_hex()))?, card: Lazy::Commit { export: git.export.to_path_buf(), oid, ribbon: ribbon.clone(), branch: branch.clone(), }, }); } Ok(planned)}
fn ticket_card( did: &str, ribbon: &Ribbon, ticket: SocialTicket, git: Option<&GitRepo>,) -> anyhow::Result<Planned> {13 unmodified lines date, body, }; return Ok(Planned { path: format!("{did}/issues/{}.png", ticket.rkey), path: format!("issues/{}.png", ticket.rkey), key: key(&card)?, card: Lazy::Ready(card), }); };24 unmodified lines }, _ => Lazy::Ready(card), }; Ok(Planned { path: format!("{did}/pulls/{}.png", ticket.rkey), path: format!("pulls/{}.png", ticket.rkey), key, card, })}33 unmodified lines1234567149 unmodified linesExpand all1571581591601581591601611611621631641651663 unmodified lines1701711721731741751761771781791801811821831841851861871881891901911921931941951961741751761771781791801811821831841851861871881971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352361911921931942372382392402412422432442452462472482492502512522532541961972552562572582592602612622632642652662672681992692702712722732022742752762772782792802812822835 unmodified linesuse std::collections::BTreeSet;use std::time::Duration;
use super::*;
fn ribbon() -> Ribbon { Ribbon {149 unmodified linesExpand all assert!(lines[1].ends_with('…'), "{lines:?}"); assert!(canvas::Face::SansBold.width(60.0, &lines[0]) <= 800.0);}
#[test]fn emit_draws_a_card_once_links_it_and_drops_what_nothing_names() { let root = tempfile::tempdir().unwrap(); let card = |path: &str, title: &str| Planned {fn ticket(path: &str, title: &str) -> Planned { Planned { path: path.into(), key: key(&title).unwrap(), card: Lazy::Ready(Card::Ticket { ribbon: ribbon(),3 unmodified lines author: "@ptr.pet".into(), date: "2026-10-28".into(), body: String::new(), }), }}
/// a repo whose one ticket is titled `title`fn set(title: &str) -> Set { Set { did: "did:plc:repo".into(), name: key(&("set", title)).unwrap(), cards: vec![ticket(REPO_CARD, title), ticket("issues/1.png", title)], }}
/// what generate does: plan the set, drawn unless it's there, and emit it into a generationfn generation(root: &Path, id: &str, title: &str) -> PathBuf { let dir = root.join("generations").join(id); let mut set = set(title); if root.join(CARDS_DIR).join(SETS_DIR).join(&set.name).is_dir() { set.cards.clear(); } let plan = Plan { sets: vec![set], users: vec![ticket("did:plc:owner.png", "owner")], }; let first = root.path().join("first"); emit( root.path(), &first, vec![card("r/issues/1.png", "one"), card("r/issues/2.png", "two")], ) .unwrap(); let drawn = root .path() .join(CARDS_DIR) .join(format!("{}.png", key(&"one").unwrap())); let linked = first.join("www").join(WWW_DIR).join("r/issues/1.png"); assert_eq!( std::fs::read(&linked).unwrap(), std::fs::read(&drawn).unwrap() emit(root, &dir, plan).unwrap(); dir}
fn served(generation: &Path, path: &str) -> std::fs::Metadata { std::fs::metadata(generation.join("www").join(WWW_DIR).join(path)).unwrap()}
#[test]fn a_set_is_drawn_once_and_every_generation_links_it() { use std::os::unix::fs::MetadataExt as _; let root = tempfile::tempdir().unwrap(); let first = generation(root.path(), "a", "one"); let second = generation(root.path(), "b", "one"); let www = first.join("www").join(WWW_DIR); assert!( std::fs::symlink_metadata(www.join("did:plc:repo")) .unwrap() .is_symlink() ); assert!( std::fs::symlink_metadata(www.join("did:plc:repo.png")) .unwrap() .is_symlink() ); for path in [ "did:plc:repo.png", "did:plc:repo/issues/1.png", "did:plc:owner.png", ] { assert_eq!( served(&first, path).ino(), served(&second, path).ino(), "{path}" ); } let sets = std::fs::read_dir(root.path().join(CARDS_DIR).join(SETS_DIR)).unwrap(); assert_eq!(sets.count(), 1);}
// the same card isn't drawn again: a marker in the drawn file survives the next emit crate::cas::atomic_write(&drawn, b"kept").unwrap(); let second = root.path().join("second"); emit(root.path(), &second, vec![card("r/issues/1.png", "one")]).unwrap();#[test]fn a_sweep_keeps_what_a_kept_generation_names() { use std::os::unix::fs::MetadataExt as _; let root = tempfile::tempdir().unwrap(); let old = generation(root.path(), "a", "one"); let new = generation(root.path(), "b", "two"); let named = BTreeSet::from([set("two").name]); sweep(root.path(), &named, &BTreeSet::new(), Duration::ZERO).unwrap();
// a rollback to the older generation still serves its own ticket card let drawn = |title: &str| { let file = root .path() .join(CARDS_DIR) .join(format!("{}.png", key(&title).unwrap())); std::fs::metadata(file).unwrap().ino() }; assert_eq!( std::fs::read(second.join("www").join(WWW_DIR).join("r/issues/1.png")).unwrap(), b"kept" served(&old, "did:plc:repo/issues/1.png").ino(), drawn("one") ); assert_eq!( served(&new, "did:plc:repo/issues/1.png").ino(), drawn("two") );
// once its generation is gone, so are its set and the cards only it linked std::fs::remove_dir_all(&old).unwrap(); sweep(root.path(), &named, &BTreeSet::new(), Duration::ZERO).unwrap(); let sets = root.path().join(CARDS_DIR).join(SETS_DIR); assert!(!sets.join(set("one").name).exists()); assert!(sets.join(set("two").name).is_dir()); let left: Vec<_> = std::fs::read_dir(root.path().join(CARDS_DIR)) let cards: Vec<_> = std::fs::read_dir(root.path().join(CARDS_DIR)) .unwrap() .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) .filter(|name| name.ends_with(".png")) .collect(); assert_eq!(left.len(), 1, "the card nothing names is gone"); assert_eq!( cards, [format!("{}.png", key(&"two").unwrap())], "{cards:?}" ); assert_eq!(served(&new, "did:plc:repo.png").ino(), drawn("two"));}
#[test]fn a_message_splits_into_its_subject_and_its_body_as_prose() {5 unmodified lines111 unmodified linesExpand all1121131141151161171181191201211221231241161171181191201211221231241251261271281338 unmodified linesExpand all111 unmodified linesExpand all crate::index_page::FAVICON_PNG, ) .unwrap();
for (card, bytes) in [ ("did:plc:repo.png", "repo card"), ("did:plc:repo/issues/1.png", "ticket card"), ] { crate::cas::atomic_write( &www.join(crate::og_card::WWW_DIR).join(card), bytes.as_bytes(), ) .unwrap(); // a repo's cards are a set outside the generation, which it links to let set = temp.path().join("og-set"); for (card, bytes) in [("repo.png", "repo card"), ("issues/1.png", "ticket card")] { crate::cas::atomic_write(&set.join(card), bytes.as_bytes()).unwrap(); } let og = www.join(crate::og_card::WWW_DIR); std::fs::create_dir_all(&og).unwrap(); std::os::unix::fs::symlink(&set, og.join("did:plc:repo")).unwrap(); std::os::unix::fs::symlink(set.join("repo.png"), og.join("did:plc:repo.png")).unwrap();
let git_dir = www .join("_git") .join(repo_token("did:plc:repo"))1338 unmodified linesExpand all