file.rs
raw
//! 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<DirEntry>),
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<App>>, Path(repo): Path<String>) -> Result<impl IntoResponse> {
page_for(&app, &repo, "").await
}
#[handler]
pub async fn page(
app: Data<&Arc<App>>,
Path((repo, path)): Path<(String, String)>,
) -> Result<impl IntoResponse> {
page_for(&app, &repo, &path).await
}
async fn page_for(app: &App, repo: &str, path: &str) -> Result<impl IntoResponse + use<>> {
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<App>>,
Path((repo, path)): Path<(String, String)>,
) -> Result<Response> {
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()))
}