From aa38e6bf2646e093c9cbb8acc2d712cab1227e3b Mon Sep 17 00:00:00 2001 From: dawn Date: Sat, 25 Jul 2026 17:48:36 +0300 Subject: [PATCH] web: serve raw repo files Signed-off-by: dawn --- api/tangled/repoblob.go | 2 +- lexicons/repo/blob.json | 4 +- web/src/lib/api/knot.ts | 11 ++++- .../lexicons/types/sh/tangled/repo/blob.ts | 4 +- .../[repo]/raw/[ref]/[...path]/+server.ts | 42 +++++++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 web/src/routes/[handle]/[repo]/raw/[ref]/[...path]/+server.ts diff --git a/api/tangled/repoblob.go b/api/tangled/repoblob.go index 3e5d6ee3..ee05c44a 100644 --- a/api/tangled/repoblob.go +++ b/api/tangled/repoblob.go @@ -72,7 +72,7 @@ type RepoBlob_Submodule struct { // path: Path to the file within the repository // raw: Return raw file content instead of JSON response // ref: Git reference (branch, tag, or commit SHA) -// repo: DID of the repository +// repo: AT-URI of the sh.tangled.repo record func RepoBlob(ctx context.Context, c util.LexClient, path string, raw bool, ref string, repo string) (*RepoBlob_Output, error) { var out RepoBlob_Output diff --git a/lexicons/repo/blob.json b/lexicons/repo/blob.json index cf6541a5..97e7b6bd 100644 --- a/lexicons/repo/blob.json +++ b/lexicons/repo/blob.json @@ -14,8 +14,8 @@ "properties": { "repo": { "type": "string", - "format": "did", - "description": "DID of the repository" + "format": "at-uri", + "description": "AT-URI of the sh.tangled.repo record" }, "ref": { "type": "string", diff --git a/web/src/lib/api/knot.ts b/web/src/lib/api/knot.ts index 1e2ccb81..49c92ab6 100644 --- a/web/src/lib/api/knot.ts +++ b/web/src/lib/api/knot.ts @@ -1,5 +1,5 @@ import type { BobbinContext, QueryValue, XrpcRequestInit } from "./client"; -import { jsonGet, rawGet } from "./_request"; +import { buildUrl, jsonGet, rawGet } from "./_request"; import type * as Archive from "./lexicons/types/sh/tangled/repo/archive"; import type * as Blob_ from "./lexicons/types/sh/tangled/repo/blob"; import type * as Branch from "./lexicons/types/sh/tangled/repo/branch"; @@ -92,3 +92,12 @@ export const tag = (ctx: BobbinContext, params: Tag.$params, init?: export const archive = (ctx: BobbinContext, params: Archive.$params, init?: XrpcRequestInit) => rawGet(ctx, "sh.tangled.repo.archive", asParams(params), init); + +// the knot picks the content type and only serves images, video and text, so a +// reader can go straight here instead of us proxying it +export const blobRawUrl = ( + serviceUrl: string, + // not `Blob_.$params`, the record wrappers hand back plain strings and + // nothing here would satisfy the generated at-uri brand + params: { repo: string; ref: string; path: string } +): string => buildUrl(serviceUrl, "sh.tangled.repo.blob", { ...params, raw: true }).toString(); diff --git a/web/src/lib/api/lexicons/types/sh/tangled/repo/blob.ts b/web/src/lib/api/lexicons/types/sh/tangled/repo/blob.ts index 3df82db7..88339f7f 100644 --- a/web/src/lib/api/lexicons/types/sh/tangled/repo/blob.ts +++ b/web/src/lib/api/lexicons/types/sh/tangled/repo/blob.ts @@ -38,9 +38,9 @@ const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.blob", { */ ref: /*#__PURE__*/ v.string(), /** - * DID of the repository + * AT-URI of the sh.tangled.repo record */ - repo: /*#__PURE__*/ v.didString(), + repo: /*#__PURE__*/ v.resourceUriString(), }), output: { type: "lex", diff --git a/web/src/routes/[handle]/[repo]/raw/[ref]/[...path]/+server.ts b/web/src/routes/[handle]/[repo]/raw/[ref]/[...path]/+server.ts new file mode 100644 index 00000000..22e970a0 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/raw/[ref]/[...path]/+server.ts @@ -0,0 +1,42 @@ +import { error } from "@sveltejs/kit"; +import { createBobbinClient } from "$lib/api/client"; +import { resolveMiniDoc } from "$lib/api/identity"; +import { blobRawUrl } from "$lib/api/knot"; +import { toHttpError } from "$lib/api/load"; +import { resolveRepoByName } from "$lib/api/repo"; +import { getConfig } from "$lib/server/config"; +import type { RequestHandler } from "./$types"; + +// redirecting instead of proxying keeps repo content off our own origin, same +// as camo and avatar +export const GET: RequestHandler = async (event) => { + const identifier = decodeURIComponent(event.params.handle); + const name = decodeURIComponent(event.params.repo); + const { ref, path } = event.params; + + // rejects bare words so unrelated paths 404 instead of resolving as actors + if (!identifier.startsWith("did:") && !identifier.includes(".")) { + error(404, "Not found"); + } + if (path === "") error(404, "Not found"); + + const { bobbinUrl } = getConfig(); + const ctx = createBobbinClient({ serviceUrl: bobbinUrl, fetch: event.fetch }); + + const doc = await resolveMiniDoc(ctx, identifier).catch((cause) => + toHttpError(cause, "Could not resolve user") + ); + const view = await resolveRepoByName(ctx, doc.did, name).catch((cause) => + toHttpError(cause, "Could not load repository") + ); + if (!view) error(404, `${doc.handle}/${name} does not exist`); + + // a branch points at a new commit after every push, so this cannot cache + return new Response(null, { + status: 302, + headers: { + location: blobRawUrl(bobbinUrl, { repo: view.uri, ref, path }), + "cache-control": "public, no-cache" + } + }); +}; -- 2.51.2