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<RepoBlobView> => {
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<ReturnType<typeof gitBlob>> | 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
};
};