b7cd2b1
use crate::rpcgit::commit::types::TreeEntryMode;
use export::http::{Path, Query, Response, State};
use maud::html;
use crate::handler::html::{
HtmlRepoPath, archived_class, error, format_size, resolve_view, response_html,
};
use crate::service::GitService;
use crate::share::access::BasicAuth;
#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
pub struct BlobContentQuery {
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub path: Option<String>,
}
#[utoipa::path(
get,
path = "/repos/{space_name}/{repo_name}/html/blob-content",
tag = "html",
description = "渲染文件内容代码表、图片或二进制提示片段。",
operation_id = "render_blob_content",
params(HtmlRepoPath, BlobContentQuery),
responses(
(status = 200, description = "blob content html fragment", content_type = "text/html"),
(status = 404, description = "repo, ref or blob not found")
)
)]
pub async fn render_blob_content(
State(service): State<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<BlobContentQuery>,
auth: BasicAuth,
) -> Response {
let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
Ok(v) => v,
Err(resp) => return resp,
};
let ctx = view.ctx;
let revision = view.revision;
let clean_path = view.clean_path;
let baby = service.baby(&ctx.ctx);
let entry = match baby
.tree_entry_at(&revision, std::path::Path::new(&clean_path))
.await
{
Ok(Some(e))
if e.mode == TreeEntryMode::Blob
|| e.mode == TreeEntryMode::Exec
|| e.mode == TreeEntryMode::Symlink =>
{
e
}
Ok(_) => return error::html_err(404, "blob not found"),
Err(_) => return error::html_err(404, "path not found"),
};
let blob = match service
.blob_content(
ctx.space_name.clone(),
ctx.repo_name.clone(),
entry.oid.to_string(),
)
.await
{
Ok(b) => b,
Err(_) => return error::html_err(404, "failed to load blob content"),
};
let base = "git-blob-content overflow-x-auto rounded-b-lg border-x border-b border-zinc-200 bg-white text-xs";
let cls = archived_class(&ctx.dto, base);
let raw_url = format!(
"/{}/{}/blob/raw/{revision}/{clean_path}",
ctx.space_name, ctx.repo_name
);
let file_name = clean_path
.rsplit('/')
.next()
.unwrap_or(&clean_path)
.to_string();
let is_image = is_image_extension(&file_name);
let markup = html! {
div class=(cls)
data-component="git-blob-content"
data-space=(ctx.space_name)
data-repo=(ctx.repo_name)
data-revision=(revision)
data-path=(clean_path.as_str())
data-size=(blob.size)
data-binary=(if blob.is_binary { "true" } else { "false" })
data-truncated=(if blob.truncated { "true" } else { "false" })
data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) {
@if is_image {
div class="flex items-center justify-center p-8 bg-zinc-50" {
img class="max-h-96 max-w-full rounded border border-zinc-200 object-contain shadow-sm"
src=(raw_url)
alt=(file_name.as_str());
}
} @else if blob.is_binary {
div class="flex flex-col items-center justify-center gap-3 p-12 text-zinc-500" {
svg class="h-10 w-10 text-zinc-400" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {
path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z" {}
}
p class="text-sm font-medium text-zinc-700" { "Binary file not shown" }
p class="text-xs text-zinc-400" { (format_size(blob.size.max(0) as u64)) }
a class="rounded border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-50 shadow-sm"
href=(raw_url)
download {
"Download raw binary"
}
}
} @else {
@let text = String::from_utf8_lossy(&blob.data);
table class="w-full font-mono text-xs leading-5 border-collapse" {
tbody {
@for (i, line) in text.lines().enumerate() {
@let line_no = i + 1;
tr class="hover:bg-zinc-50/80 group" id=(format!("L{line_no}")) {
td class="w-12 select-none border-r border-zinc-200 bg-zinc-50/50 px-2 py-0 text-right text-zinc-400 group-hover:text-zinc-600 font-mono text-[11px]"
data-line=(line_no) {
a class="hover:underline text-zinc-400" href=(format!("#L{line_no}")) {
(line_no)
}
}
td class="whitespace-pre px-3 py-0 font-mono text-zinc-900 overflow-x-auto" {
(line)
}
}
}
}
}
@if blob.truncated {
div class="border-t border-amber-200 bg-amber-50 p-2 text-center text-xs text-amber-700" {
"File is too large and has been truncated. "
a class="font-medium underline" href=(raw_url) { "Download raw file" }
}
}
}
}
};
response_html(markup, 30)
}
fn is_image_extension(filename: &str) -> bool {
let lower = filename.to_ascii_lowercase();
lower.ends_with(".png")
|| lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".gif")
|| lower.ends_with(".svg")
|| lower.ends_with(".webp")
|| lower.ends_with(".ico")
|| lower.ends_with(".bmp")
|| lower.ends_with(".avif")
}