Krolyxon
Commit 18ca21c

ferreniumsrchandlerstree.rs

203 lines · 6.5 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 log_url: String,
45 pub readme: Option<Readme>,
46}
47
48pub struct Readme {
49 pub name: String,
50 pub url: String,
51 pub html: Arc<str>,
52}
53
54pub fn page(ctx: &Ctx, segments: &[Vec<u8>]) -> Response {
55 let (rev, path) = match ctx.resolve(segments) {
56 Ok(found) => found,
57 Err(res) => return *res,
58 };
59 let tree_id = match tree::lookup(&ctx.git, rev.commit, &path) {
60 Ok(Some(PathTarget::Tree(id))) => id,
61 Ok(Some(PathTarget::Blob(..))) => return redirect(&ctx.url("blob", &rev.name, &path)),
62 Ok(Some(PathTarget::Submodule(_))) => return ctx.not_found("PATH IS A SUBMODULE"),
63 Ok(None) => {
64 return ctx.not_found(&format!("PATH NOT FOUND IN {}", rev.name));
65 }
66 Err(e) => return ctx.internal_error(e),
67 };
68
69 let etag = ctx.etag(("tree", tree_id, &rev.name));
70 let immutable = rev.is_immutable();
71 if let Some(res) = ctx.not_modified(&etag, immutable) {
72 return res;
73 }
74 let entries = match listing(ctx, tree_id) {
75 Ok(entries) => entries,
76 Err(e) => return ctx.internal_error(e),
77 };
78
79 let (crumbs, here) = crumbs(ctx, &rev.name, &path);
80 let mut rows = rows(ctx, &rev, &path, &entries);
81 if !path.is_empty() {
82 let up = path.rsplit_once_str("/").map_or(&b""[..], |(dir, _)| dir);
83 rows.insert(
84 0,
85 TreeRow {
86 name: "..".into(),
87 url: Some(ctx.url("tree", &rev.name, up)),
88 icon: "folder",
89 meta: String::new(),
90 },
91 );
92 }
93 let title = match &here {
94 Some(_) => format!("{} · {}", path.to_str_lossy(), ctx.repo.name),
95 None => ctx.repo.name.clone(),
96 };
97 let page = TreePage {
98 layout: Layout::new(ctx.state, ctx.config, Some(&title)),
99 nav: ctx.nav(Active::Tree, Some(&rev), |name| {
100 ctx.url("tree", name, &path)
101 }),
102 crumbs,
103 here,
104 rows,
105 entries: entries.len(),
106 tree_id: short(&tree_id.to_string()),
107 log_url: ctx.url("log", &rev.name, &path),
108 readme: dir_readme(ctx, &rev, &path, &entries),
109 };
110 let mut res = render(StatusCode::OK, &page);
111 set_cache_headers(&mut res, &etag, immutable);
112 res
113}
114
115/// Cached listing of a tree.
116pub fn listing(ctx: &Ctx, tree_id: ObjectId) -> anyhow::Result<Arc<Vec<TreeEntry>>> {
117 ctx.state
118 .cache
119 .tree(tree_id, || tree::list(&ctx.git, tree_id))
120}
121
122pub fn rows(ctx: &Ctx, rev: &Rev, dir: &[u8], entries: &[TreeEntry]) -> Vec<TreeRow> {
123 entries
124 .iter()
125 .map(|entry| {
126 let mut path = dir.to_vec();
127 if !path.is_empty() {
128 path.push(b'/');
129 }
130 path.extend_from_slice(&entry.name);
131 let (action, icon) = match entry.kind {
132 EntryKind::Tree => ("tree", "folder"),
133 EntryKind::Symlink => ("blob", "link"),
134 EntryKind::Submodule => ("", "package"),
135 EntryKind::Blob | EntryKind::Executable => ("blob", "file"),
136 };
137 TreeRow {
138 name: entry.name.to_str_lossy().into_owned(),
139 url: (!action.is_empty()).then(|| ctx.url(action, &rev.name, &path)),
140 icon,
141 meta: match entry.kind {
142 EntryKind::Submodule => format!("@ {}", short(&entry.oid.to_string())),
143 _ => entry.size.map(human_size).unwrap_or_default(),
144 },
145 }
146 })
147 .collect()
148}
149
150/// Names tried for a directory README, in order of preference.
151const README_NAMES: &[&str] = &["readme.md", "readme.markdown", "readme", "readme.txt"];
152
153fn dir_readme(ctx: &Ctx, rev: &Rev, dir: &[u8], entries: &[TreeEntry]) -> Option<Readme> {
154 let entry = README_NAMES.iter().find_map(|want| {
155 entries.iter().find(|e| {
156 e.kind.is_blob()
157 && e.kind != EntryKind::Symlink
158 && e.name.eq_ignore_ascii_case(want.as_bytes())
159 })
160 })?;
161 let name = entry.name.to_str_lossy().into_owned();
162 readme(ctx, rev, dir, &name, entry.oid)
163}
164
165/// Render a README blob: Markdown as HTML, anything else as plain text.
166pub fn readme(ctx: &Ctx, rev: &Rev, dir: &[u8], name: &str, oid: ObjectId) -> Option<Readme> {
167 if tree::size(&ctx.git, oid).ok()? > MAX_README {
168 return None;
169 }
170 let links = Links {
171 repo_url: ctx.repo_url(),
172 rev: encode_path(&rev.name),
173 dir: dir.to_vec(),
174 };
175 let is_md = markdown::is_markdown(name);
176 let key = Key::Markdown {
177 blob: oid,
178 context: if is_md { links.key() } else { String::new() },
179 };
180 let html = ctx
181 .state
182 .cache
183 .html(key, || -> anyhow::Result<String> {
184 let data = tree::blob(&ctx.git, oid)?;
185 let text = String::from_utf8_lossy(&data);
186 Ok(if is_md {
187 markdown::render(&text, &links)
188 } else {
189 markdown::render_plain(&text)
190 })
191 })
192 .ok()?;
193 let mut path = dir.to_vec();
194 if !path.is_empty() {
195 path.push(b'/');
196 }
197 path.extend_from_slice(name.as_bytes());
198 Some(Readme {
199 name: name.to_string(),
200 url: ctx.url("blob", &rev.name, &path),
201 html,
202 })
203}