//! The file view (a page for any path on the default branch) and the raw //! view (the bytes themselves). use std::sync::Arc; use poem::{ IntoResponse, Response, Result, handler, http::header, web::{Data, Path}, }; use sailfish::TemplateOnce; use crate::{ App, render::{self, Links, Presentation, View}, repos::{DirEntry, Entry, RepoPath}, }; #[derive(TemplateOnce)] #[template(path = "pages/file.stpl.html")] struct FilePage { path: RepoPath, links: Links, listing: Listing, } enum Listing { Dir(Vec), File(Presentation), } /// A file's content under its heading, shared by the file page and the /// README section of a repository page. #[derive(TemplateOnce)] #[template(path = "partials/presentation.stpl.html")] pub struct PresentationView<'a> { pub title: &'a str, pub title_url: Option<&'a str>, pub raw_url: &'a str, pub presentation: &'a Presentation, } #[handler] pub async fn root(app: Data<&Arc>, Path(repo): Path) -> Result { page_for(&app, &repo, "").await } #[handler] pub async fn page( app: Data<&Arc>, Path((repo, path)): Path<(String, String)>, ) -> Result { page_for(&app, &repo, &path).await } async fn page_for(app: &App, repo: &str, path: &str) -> Result> { let name = super::repo_name(repo)?; let path = super::repo_path(path)?; let links = Links::for_document(&name, &path); let (lookup_path, lookup_links) = (path.clone(), links.clone()); let listing = app .blocking(move |store| { let Some(repo) = store.open(lookup_links.repo())? else { return Ok(None); }; Ok(match repo.entry(&lookup_path)? { None => None, Some(Entry::Dir(entries)) => Some(Listing::Dir(entries)), Some(Entry::File(file)) => Some(Listing::File(Presentation::of( &repo, &file, &lookup_links, )?)), }) }) .await?; let listing = super::found(listing)?; let title = if path.is_root() { name.to_string() } else { format!("{path} ยท {name}") }; Ok(super::page( app, title, FilePage { path, links, listing, }, )?) } /// The bytes of a file, as `git cat-file` produces them: a file of any /// size streams to the client rather than being held in memory, which /// libgit2 could only do for a loose object. The media type is judged /// from the file's name and its first bytes. #[handler] pub async fn raw( app: Data<&Arc>, Path((repo, path)): Path<(String, String)>, ) -> Result { let name = super::repo_name(&repo)?; let path = super::repo_path(&path)?; let lookup_name = name.clone(); let file = app .blocking(move |store| { let Some(repo) = store.open(&lookup_name)? else { return Ok(None); }; Ok(match repo.entry(&path)? { Some(Entry::File(file)) => Some(file), _ => None, }) }) .await?; let file = super::found(file)?; let git_dir = app.store.path(&name); let object = file.id().to_string(); let mut process = app .git .start("cat-file", None, move |command| { command .arg("--git-dir") .arg(git_dir) .args(["cat-file", "blob", &object]); }) .await?; let file_name = file.path.file_name().unwrap_or_default(); let mime = render::mime_type(file_name, process.peek().await?); Ok(Response::builder() .header(header::CONTENT_TYPE, mime.to_string()) .header(header::CONTENT_LENGTH, file.size) // Whatever the type, a raw file opened directly is a document of // its own that cannot run script or reach the site's origin. .header(header::CONTENT_SECURITY_POLICY, "sandbox") .body(process.into_body())) }