Krolyxon
Commit 55c1225

ferreniumsrchandlerstree.rs

201 lines · 6.4 KB · Rust

1//! `/{repo}/tree/{rev}/{path…}` — directory listing, plus the directory's
2//! README. Also provides the listing and README rendering used by the
3//! summary page.
4
5use std::sync::Arc;
6
7use askama::Template;
8use axum::http::StatusCode;
9use axum::response::Response;
10use gix::bstr::ByteSlice;
11
12use super::repo::{Active, Crumb, Ctx, RepoNav, crumbs, redirect, set_cache_headers, short};
13use super::{Layout, render};
14use crate::cache::Key;
15use crate::git::ObjectId;
16use crate::git::refs::Rev;
17use crate::git::tree::{self, EntryKind, PathTarget, TreeEntry};
18use crate::render::markdown::{self, Links};
19use crate::render::{encode_path, human_size};
20
21/// READMEs larger than this are not rendered inline.
22const MAX_README: u64 = 512 * 1024;
23
24pub struct TreeRow {
25 pub name: String,
26 /// `None` for submodules (nothing to browse here).
27 pub url: Option<String>,
28 pub icon: &'static str,
29 /// Size for files, short commit id for submodules.
30 pub meta: String,
31}
32
33#[derive(Template)]
34#[template(path = "tree.html")]
35pub struct TreePage {
36 pub layout: Layout,
37 pub nav: RepoNav,
38 pub crumbs: Vec<Crumb>,
39 pub here: Option<String>,
40 /// Includes a leading `..` row below the root.
41 pub rows: Vec<TreeRow>,
42 pub entries: usize,
43 pub tree_id: String,
44 pub readme: Option<Readme>,
45}
46
47pub struct Readme {
48 pub name: String,
49 pub url: String,
50 pub html: Arc<str>,
51}
52
53pub fn page(ctx: &Ctx, segments: &[Vec<u8>]) -> Response {
54 let (rev, path) = match ctx.resolve(segments) {
55 Ok(found) => found,
56 Err(res) => return *res,
57 };
58 let tree_id = match tree::lookup(&ctx.git, rev.commit, &path) {
59 Ok(Some(PathTarget::Tree(id))) => id,
60 Ok(Some(PathTarget::Blob(..))) => return redirect(&ctx.url("blob", &rev.name, &path)),
61 Ok(Some(PathTarget::Submodule(_))) => return ctx.not_found("PATH IS A SUBMODULE"),
62 Ok(None) => {
63 return ctx.not_found(&format!("PATH NOT FOUND IN {}", rev.name));
64 }
65 Err(e) => return ctx.internal_error(e),
66 };
67
68 let etag = ctx.etag(("tree", tree_id, &rev.name));
69 let immutable = rev.is_immutable();
70 if let Some(res) = ctx.not_modified(&etag, immutable) {
71 return res;
72 }
73 let entries = match listing(ctx, tree_id) {
74 Ok(entries) => entries,
75 Err(e) => return ctx.internal_error(e),
76 };
77
78 let (crumbs, here) = crumbs(ctx, &rev.name, &path);
79 let mut rows = rows(ctx, &rev, &path, &entries);
80 if !path.is_empty() {
81 let up = path.rsplit_once_str("/").map_or(&b""[..], |(dir, _)| dir);
82 rows.insert(
83 0,
84 TreeRow {
85 name: "..".into(),
86 url: Some(ctx.url("tree", &rev.name, up)),
87 icon: "folder",
88 meta: String::new(),
89 },
90 );
91 }
92 let title = match &here {
93 Some(_) => format!("{} · {}", path.to_str_lossy(), ctx.repo.name),
94 None => ctx.repo.name.clone(),
95 };
96 let page = TreePage {
97 layout: Layout::new(ctx.state, ctx.config, Some(&title)),
98 nav: ctx.nav(Active::Tree, Some(&rev), |name| {
99 ctx.url("tree", name, &path)
100 }),
101 crumbs,
102 here,
103 rows,
104 entries: entries.len(),
105 tree_id: short(&tree_id.to_string()),
106 readme: dir_readme(ctx, &rev, &path, &entries),
107 };
108 let mut res = render(StatusCode::OK, &page);
109 set_cache_headers(&mut res, &etag, immutable);
110 res
111}
112
113/// Cached listing of a tree.
114pub fn listing(ctx: &Ctx, tree_id: ObjectId) -> anyhow::Result<Arc<Vec<TreeEntry>>> {
115 ctx.state
116 .cache
117 .tree(tree_id, || tree::list(&ctx.git, tree_id))
118}
119
120pub fn rows(ctx: &Ctx, rev: &Rev, dir: &[u8], entries: &[TreeEntry]) -> Vec<TreeRow> {
121 entries
122 .iter()
123 .map(|entry| {
124 let mut path = dir.to_vec();
125 if !path.is_empty() {
126 path.push(b'/');
127 }
128 path.extend_from_slice(&entry.name);
129 let (action, icon) = match entry.kind {
130 EntryKind::Tree => ("tree", "folder"),
131 EntryKind::Symlink => ("blob", "link"),
132 EntryKind::Submodule => ("", "package"),
133 EntryKind::Blob | EntryKind::Executable => ("blob", "file"),
134 };
135 TreeRow {
136 name: entry.name.to_str_lossy().into_owned(),
137 url: (!action.is_empty()).then(|| ctx.url(action, &rev.name, &path)),
138 icon,
139 meta: match entry.kind {
140 EntryKind::Submodule => format!("@ {}", short(&entry.oid.to_string())),
141 _ => entry.size.map(human_size).unwrap_or_default(),
142 },
143 }
144 })
145 .collect()
146}
147
148/// Names tried for a directory README, in order of preference.
149const README_NAMES: &[&str] = &["readme.md", "readme.markdown", "readme", "readme.txt"];
150
151fn dir_readme(ctx: &Ctx, rev: &Rev, dir: &[u8], entries: &[TreeEntry]) -> Option<Readme> {
152 let entry = README_NAMES.iter().find_map(|want| {
153 entries.iter().find(|e| {
154 e.kind.is_blob()
155 && e.kind != EntryKind::Symlink
156 && e.name.eq_ignore_ascii_case(want.as_bytes())
157 })
158 })?;
159 let name = entry.name.to_str_lossy().into_owned();
160 readme(ctx, rev, dir, &name, entry.oid)
161}
162
163/// Render a README blob: Markdown as HTML, anything else as plain text.
164pub fn readme(ctx: &Ctx, rev: &Rev, dir: &[u8], name: &str, oid: ObjectId) -> Option<Readme> {
165 if tree::size(&ctx.git, oid).ok()? > MAX_README {
166 return None;
167 }
168 let links = Links {
169 repo_url: ctx.repo_url(),
170 rev: encode_path(&rev.name),
171 dir: dir.to_vec(),
172 };
173 let is_md = markdown::is_markdown(name);
174 let key = Key::Markdown {
175 blob: oid,
176 context: if is_md { links.key() } else { String::new() },
177 };
178 let html = ctx
179 .state
180 .cache
181 .html(key, || -> anyhow::Result<String> {
182 let data = tree::blob(&ctx.git, oid)?;
183 let text = String::from_utf8_lossy(&data);
184 Ok(if is_md {
185 markdown::render(&text, &links)
186 } else {
187 markdown::render_plain(&text)
188 })
189 })
190 .ok()?;
191 let mut path = dir.to_vec();
192 if !path.is_empty() {
193 path.push(b'/');
194 }
195 path.extend_from_slice(name.as_bytes());
196 Some(Readme {
197 name: name.to_string(),
198 url: ctx.url("blob", &rev.name, &path),
199 html,
200 })
201}