import { error } from "@sveltejs/kit"; import { getRequestEvent } from "$app/server"; import { classifyBlob, contentStats, countLines, formatBytes, hasRenderedView, hasTextView, MAX_INLINE_SIZE, type RepoBlobView } from "#lib/api/blob.ts"; import { ClientResponseError } from "#lib/api/client.ts"; import { blob as gitBlob, gitTarget, requireGit } from "#lib/api/gitclient.ts"; import { toHttpError } from "#lib/api/load.ts"; import { toTreeCommitSummary } from "#lib/api/repo.ts"; import { withAuthorHandles } from "#lib/api/repoIndex.ts"; import type { StringRecord } from "#lib/api/records.ts"; import { isMarkdownFile } from "#lib/markup/index.ts"; import type { RepoLoadEvent, RepoParent } from "#lib/server/repoIndex.ts"; export const loadRepoBlob = async ( event: RepoLoadEvent, parent: RepoParent, ref: string, path: string ): Promise => { const { fetch } = getRequestEvent(); const repo = requireGit(await parent.repo); const git = gitTarget(parent.publicConfig, repo, fetch); // the knot 404s directories and missing paths alike // knot2's repo_blob answers 413 over its serving limit instead of // sending size json (knot-xrpc reads.rs) const output = await gitBlob(git, { ref, path }, MAX_INLINE_SIZE).catch( (cause: unknown): Awaited> | null => { if (cause instanceof ClientResponseError && cause.status === 404) { error(404, `${path} does not exist at ${ref}`); } if ( cause instanceof ClientResponseError && (cause.status === 413 || cause.error === "BlobTooLarge") ) { return null; } return toHttpError(cause, "Could not load file"); } ); if (output === null) { return { ref, path, kind: "other", sizeLabel: null, lines: null, contents: null, fileTooLarge: true, defaultView: "code", submodule: null, lastCommit: null }; } const kind = classifyBlob(output); const size = output.size ?? null; const fileTooLarge = output.fileTooLarge === true || (hasTextView(kind) && size !== null && size > MAX_INLINE_SIZE); const contents = hasTextView(kind) && !fileTooLarge && output.encoding === "utf-8" ? (output.content ?? null) : null; const summary = output.lastCommit ? toTreeCommitSummary({ hash: output.lastCommit.hash, message: output.lastCommit.message, when: output.lastCommit.when, author: output.lastCommit.author }) : null; const [lastCommit = null] = summary ? await withAuthorHandles([summary], parent.publicConfig.bobbinUrl, fetch) : []; return { ref, path, kind, sizeLabel: size === null ? null : formatBytes(size), lines: contents !== null ? countLines(contents) : null, contents, fileTooLarge, defaultView: hasRenderedView(kind) ? event.url.searchParams.has("code") ? "code" : "rendered" : "code", submodule: output.submodule ? { name: output.submodule.name, url: output.submodule.url } : null, lastCommit }; }; export interface StringBlobSource { rkey: string; code: boolean; } export const loadStringBlob = ( record: StringRecord, contents: string, { rkey, code }: StringBlobSource ): RepoBlobView => { const filename = record.snippet.name; const stats = contentStats(contents); const fileTooLarge = stats.bytes > MAX_INLINE_SIZE; const kind = isMarkdownFile(filename) ? "markup" : "code"; return { ref: rkey, path: filename, kind, sizeLabel: stats.sizeLabel, lines: fileTooLarge ? null : stats.lines, contents: fileTooLarge ? null : contents, fileTooLarge, defaultView: kind === "markup" && !code ? "rendered" : "code", submodule: null, lastCommit: null }; };