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()))
}