lmjtfy.git / apps / lmjtfy / src / browse.rs
browse.rsannotatedbrowse.rssource264 lines · 10.7 KB · raw

/<repo>.git/<path> in a browser: the code, browsed. A directory lists its entries and shows its README; a file is shown with numbered lines, or rendered if it is markdown; ?raw serves it as it is.

Everything comes from GitHub's contents API with the clone proxy's read-only token, at most once a minute per isolate for a given path. jevcrates is browsed on its own and where each project pins it: a path under a project's jevcrates submodule is read from jevcrates at the pinned commit.

11use std::cell::RefCell;
13use axum::body::Body;
14use axum::extract::{Path, Query, State};
15use axum::http::{HeaderMap, Response, StatusCode, header};
16use serde::Deserialize;
17use tree::{Contents, Kind};
18
19use super::App;
20use crate::clone::{self, Repo};
21use crate::host::{GitRedirect, Host};
22use crate::view::code::{self, Doc, Docs, Shown, View};

How long an isolate keeps what GitHub said about a path.

25const KEPT_MS: f64 = 60_000.0;

How many paths an isolate keeps at once; the oldest goes first.

27const KEPT: usize = 64;
29thread_local! {

What GitHub said, by (repository, ref, path), and when.

31    static READ: RefCell<Vec<((Repo, String, String), f64, Read)>> = const { RefCell::new(Vec::new()) };

Each repository's whole tree at main, submodules mounted, and when.

33    static TREES: RefCell<Vec<(Repo, f64, tree::Tree)>> = const { RefCell::new(Vec::new()) };
34}

The whole tree of repo at main for the explorer, each submodule's tree mounted where it is pinned: two calls to GitHub at most, kept a minute. Empty if GitHub cannot be read; the pages still work without it.

39pub async fn explorer(env: &worker::Env, repo: Repo) -> tree::Tree {
40    let now = js_sys::Date::now();
41    let kept = TREES.with(|trees| trees.borrow().iter().find(|(seen, at, _)| *seen == repo && now - at < KEPT_MS).map(|(_, _, tree)| tree.clone()));
42    if let Some(tree) = kept {
43        return tree;
44    }
45    let Some(mut whole) = git_tree(env, repo, repo.branch()).await else { return tree::Tree::default() };
46    for (mount, inner) in repo.submodules() {
47        let pin = whole.pins.iter().find(|(path, _)| path == mount).map(|(_, sha)| sha.clone());
48        if let Some(inner_tree) = match pin { Some(sha) => git_tree(env, *inner, &sha).await, None => None } {
49            whole.mount(mount, inner_tree);
50        }
51    }
52    TREES.with(|trees| {
53        let mut trees = trees.borrow_mut();
54        trees.retain(|(seen, _, _)| *seen != repo);
55        trees.push((repo, now, whole.clone()));
56    });
57    whole
58}
60async fn git_tree(env: &worker::Env, repo: Repo, reference: &str) -> Option<tree::Tree> {
61    let url = format!("https://api.github.com/repos/{}/git/trees/{}?recursive=1", repo.github(), tree::encoded(&[reference]));
62    match clone::github(env, &url).await? {
63        (200, body, _) => tree::parse_tree(&body),
64        _ => None,
65    }
66}

What reading a path came to.

69#[derive(Clone)]
70pub enum Read {
71    Found(Contents),

GitHub said there is nothing there.

73    Missing,

GitHub could not be asked or did not answer: no token, a limit, an outage.

76    Unreachable,
77}

?raw serves a file as it is; ?view=agents shows a directory's CLAUDE.md in place of its README.

81#[derive(Deserialize)]
82pub struct Asked {
83    raw: Option<String>,
84    view: Option<String>,
85}

GET /<repo>/<path>.

88#[worker::send]
89pub async fn path(
90    State(app): State<App>,
91    Path((segment, path)): Path<(String, String)>,
92    Query(asked): Query<Asked>,
93    headers: HeaderMap,
94) -> Response<Body> {
95    let origin = super::origin(&headers);
96    let Some(repo) = Repo::named(&segment) else { return clone::refused(StatusCode::NOT_FOUND, "No such page.") };
97    let Some(segments) = tree::segments(&path) else { return clone::refused(StatusCode::NOT_FOUND, "No such page.") };
98    let served = segments.join("/");
99    let read = resolve(&app.env, repo, &segments).await;
100    if asked.raw.is_some()
101        && let Read::Found(Contents::File { path, body, .. }) = &read
102    {
103        return raw_file(path, body);
104    }
105    let view = View::asked(asked.view.as_deref());
106    let (status, shown) = match &read {
107        Read::Found(Contents::Dir(entries)) => {
108            let entries = mounted(repo, &served, entries);
109            let docs = docs(&app.env, repo, &segments, &entries, view).await;
110            (StatusCode::OK, Shown::Dir { docs })
111        }
112        Read::Found(Contents::File { size, body, .. }) => (StatusCode::OK, Shown::File { size: *size, body, view }),
113        // A submodule's own path resolves to its root, so this is not seen.
114        Read::Found(Contents::Submodule { .. }) | Read::Missing => (StatusCode::NOT_FOUND, Shown::Missing),
115        Read::Unreachable => (StatusCode::BAD_GATEWAY, Shown::Unreachable),
116    };
117    let whole = explorer(&app.env, repo).await;
118    let latest = clone::latest(&app.env, repo).await;
119    let metas = clone::metas(&app.env).await;
120    let frame = frame(&origin, super::host_of(&headers), super::git_redirect(&app.env), repo, &served, &whole, latest.as_ref(), metas);
121    let page = code::page(&frame, shown);
122    Response::builder()
123        .status(status)
124        .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
125        .header(header::CACHE_CONTROL, "no-cache")
126        .body(Body::from(page.into_string()))
127        .expect("static headers are valid")
128}

Everything around a code page's main pane, for repo. metas is what GitHub says about each repository, in the order of Repo::ALL.

132#[allow(clippy::too_many_arguments)]
133pub fn frame<'a>(
134    origin: &'a str,
135    host: Host,
136    redirect: GitRedirect,
137    repo: Repo,
138    path: &'a str,
139    tree: &'a tree::Tree,
140    latest: Option<&'a card::Commit>,
141    metas: Vec<Option<code::Meta>>,
142) -> code::Frame<'a> {
143    let at = Repo::ALL.iter().position(|each| *each == repo);
144    let meta = at.and_then(|at| metas.get(at).cloned().flatten());
145    code::Frame {
146        origin,
147        host,
148        project: repo.project(),
149        path,
150        tree,
151        latest,
152        clone: repo.command(&host.code_origin(origin, redirect)),
153        projects: Repo::ALL.iter().map(|repo| repo.project()).zip(metas.into_iter().chain(std::iter::repeat(None))).collect(),
154        meta,
155    }
156}

The root of a repository's documents, for the front page.

159pub async fn root(env: &worker::Env, repo: Repo, view: View) -> Docs {
160    match resolve(env, repo, &[]).await {
161        Read::Found(Contents::Dir(entries)) => docs(env, repo, &[], &entries, view).await,
162        _ => Docs { people: None, agents: None, view },
163    }
164}

What is at a served path: in the repository itself, or under one of its submodules, in the submodule at the commit it is pinned at.

168async fn resolve(env: &worker::Env, repo: Repo, segments: &[&str]) -> Read {
169    for (mount, inner) in repo.submodules() {
170        let mount_segments: Vec<&str> = mount.split('/').collect();
171        if segments.starts_with(&mount_segments) {
172            let pin = match read(env, repo, repo.branch(), &mount_segments).await {
173                Read::Found(Contents::Submodule { sha, .. }) => sha,
174                Read::Found(_) | Read::Missing => return Read::Missing,
175                Read::Unreachable => return Read::Unreachable,
176            };
177            return read(env, *inner, &pin, &segments[mount_segments.len()..]).await;
178        }
179    }
180    read(env, repo, repo.branch(), segments).await
181}

A directory's entries with their paths as served: a submodule's entries are under its mount.

185fn mounted(repo: Repo, served: &str, entries: &[tree::Entry]) -> Vec<tree::Entry> {
186    let mount = repo.submodules().iter().map(|(mount, _)| *mount).find(|mount| served == *mount || served.starts_with(&format!("{mount}/")));
187    entries
188        .iter()
189        .map(|entry| tree::Entry {
190            path: match mount {
191                Some(mount) => format!("{mount}/{}", entry.path),
192                None => entry.path.clone(),
193            },
194            ..entry.clone()
195        })
196        .collect()
197}

The directory's README and CLAUDE.md, rendered.

200async fn docs(env: &worker::Env, repo: Repo, segments: &[&str], entries: &[tree::Entry], view: View) -> Docs {
201    Docs {
202        people: doc(env, repo, segments, entries, "README.md").await,
203        agents: doc(env, repo, segments, entries, "CLAUDE.md").await,
204        view,
205    }
206}
208async fn doc(env: &worker::Env, repo: Repo, segments: &[&str], entries: &[tree::Entry], name: &str) -> Option<Doc> {
209    let found = entries.iter().find(|entry| entry.kind == Kind::File && entry.name.eq_ignore_ascii_case(name))?;
210    let mut path = segments.to_vec();
211    path.push(&found.name);
212    match resolve(env, repo, &path).await {
213        Read::Found(Contents::File { body: tree::Body::Text(text), .. }) => Some(Doc::new(&path.join("/"), &text, &repo.project().base())),
214        _ => None,
215    }
216}

One path of one repository at one ref, from this isolate's memory or from GitHub.

220async fn read(env: &worker::Env, repo: Repo, reference: &str, segments: &[&str]) -> Read {
221    let key = (repo, reference.to_owned(), segments.join("/"));
222    let now = js_sys::Date::now();
223    let kept = READ.with(|read| {
224        read.borrow().iter().find(|(seen, at, _)| *seen == key && now - at < KEPT_MS).map(|(_, _, read)| read.clone())
225    });
226    if let Some(read) = kept {
227        return read;
228    }
229    let url = format!("https://api.github.com/repos/{}/contents/{}?ref={}", repo.github(), tree::encoded(segments), tree::encoded(&[reference]));
230    let read = match clone::github(env, &url).await {
231        Some((200, body, _)) => tree::parse(&body).map_or(Read::Unreachable, Read::Found),
232        Some((404, _, _)) => Read::Missing,
233        _ => Read::Unreachable,
234    };
235    // Only answers are kept: an outage is asked about again next time.
236    if !matches!(read, Read::Unreachable) {
237        READ.with(|kept| {
238            let mut kept = kept.borrow_mut();
239            kept.retain(|(seen, at, _)| *seen != key && now - at < KEPT_MS);
240            if kept.len() >= KEPT {
241                kept.remove(0);
242            }
243            kept.push((key, now, read.clone()));
244        });
245    }
246    read
247}

A file as it is. A sandbox keeps anything in it from running as this site.

251fn raw_file(path: &str, body: &tree::Body) -> Response<Body> {
252    let bytes = match body {
253        tree::Body::Text(text) => text.clone().into_bytes(),
254        tree::Body::Binary(bytes) => bytes.clone(),
255        tree::Body::TooLarge => return clone::refused(StatusCode::PAYLOAD_TOO_LARGE, "Too large to show here. Clone it."),
256    };
257    Response::builder()
258        .header(header::CONTENT_TYPE, tree::media_type(path, body))
259        .header(header::CACHE_CONTROL, "public, max-age=60")
260        .header("content-security-policy", "sandbox")
261        .header("x-content-type-options", "nosniff")
262        .body(Body::from(bytes))
263        .expect("static headers are valid")
264}