SPB Git

Repository Public MIT

media.ts 5.4 KB 114 lines typescript

1import { createHash } from "node:crypto";2import { EXAMPLE_LIMITS, imageKindForPath, readArchive, sniffImage, type ExampleClient, type ManifestExample } from "@skills/registry-core";3import { ApiError, notFound } from "./errors";4import { env } from "./env";5import type { ArtifactRow, ReleaseRow } from "./registry";6import { readBlob } from "./storage";78/**9 * Usage examples (screenshots) of a release. The only files ever served are the ones the release10 * manifest lists under `examples`, read from the immutable archive and re-checked as raster images.11 * Nothing is rendered, resized or executed on the server.12 */13export interface ExampleMedia {14  path: string;15  mime: string;16  width: number;17  height: number;18  data: Buffer;19  etag: string;20}2122interface Entry {23  media: Map<string, ExampleMedia>;24  bytes: number;25}26const g = globalThis as unknown as { __skillsMedia?: Map<string, Entry> };27const cache = (g.__skillsMedia ??= new Map());28const MAX_CACHE_BYTES = 96 * 1024 * 1024;2930function listed(r: ReleaseRow): ManifestExample[] {31  const ex = r.manifest_json?.examples;32  return Array.isArray(ex) ? ex.slice(0, EXAMPLE_LIMITS.maxExamples) : [];33}3435/** Example images of one release, keyed by archive path. Cached by archive digest (content never changes). */36function loadMedia(r: ReleaseRow): Map<string, ExampleMedia> {37  const wanted = new Set(listed(r).map((e) => e.image));38  if (wanted.size === 0) return new Map();39  const hit = cache.get(r.sha256);40  if (hit) {41    cache.delete(r.sha256);42    cache.set(r.sha256, hit); // most recently used last43    return hit.media;44  }45  const media = new Map<string, ExampleMedia>();46  let bytes = 0;47  for (const f of readArchive(readBlob(r.archive_object_key))) {48    if (!wanted.has(f.path) || f.data.length > EXAMPLE_LIMITS.maxImageBytes) continue;49    const info = sniffImage(f.data);50    if (!info || info.kind !== imageKindForPath(f.path)) continue;51    const data = Buffer.from(f.data); // detach from the unpacked archive so it can be collected52    media.set(f.path, { path: f.path, mime: info.mime, width: info.width, height: info.height, data, etag: `"sha256-${createHash("sha256").update(data).digest("hex")}"` });53    bytes += data.length;54  }55  cache.set(r.sha256, { media, bytes });56  let total = 0;57  for (const e of cache.values()) total += e.bytes;58  for (const [k, e] of cache) {59    if (total <= MAX_CACHE_BYTES || k === r.sha256) break;60    cache.delete(k);61    total -= e.bytes;62  }63  return media;64}6566export const mediaUrl = (a: Pick<ArtifactRow, "namespace" | "slug">, version: string, path: string) =>67  `${env.publicUrl}/api/v1/artifacts/${a.namespace}/${a.slug}/versions/${encodeURIComponent(version)}/media/${path.split("/").map(encodeURIComponent).join("/")}`;6869export interface ExampleDto {70  title: string;71  client: ExampleClient;72  image: string;73  alt: string;74  prompt: string | null;75  caption: string | null;76  capturedAt: string | null;77  url: string;78}79/** Examples as declared in the manifest, each with the URL the registry serves it from. No archive access. */80export function exampleDtos(a: Pick<ArtifactRow, "namespace" | "slug">, r: ReleaseRow): ExampleDto[] {81  return listed(r).map((e) => ({ title: e.title, client: e.client, image: e.image, alt: e.alt, prompt: e.prompt ?? null, caption: e.caption ?? null, capturedAt: e.capturedAt ?? null, url: mediaUrl(a, r.version, e.image) }));82}8384/** Examples with the pixel size and byte size of each image (reads the archive once, then cached). */85export function examplesWithSize(a: Pick<ArtifactRow, "namespace" | "slug">, r: ReleaseRow): (ExampleDto & { width: number | null; height: number | null; sizeBytes: number | null; mime: string | null })[] {86  const dtos = exampleDtos(a, r);87  if (dtos.length === 0 || r.state === "revoked") return dtos.map((d) => ({ ...d, width: null, height: null, sizeBytes: null, mime: null }));88  let media = new Map<string, ExampleMedia>();89  try { media = loadMedia(r); } catch { /* a missing blob must not break the page; the image route reports it */ }90  return dtos.map((d) => {91    const m = media.get(d.image);92    return { ...d, width: m?.width ?? null, height: m?.height ?? null, sizeBytes: m?.data.length ?? null, mime: m?.mime ?? null };93  });94}9596/** Serve one example image. Visibility was already enforced by the artifact and release lookups. */97export function serveExampleMedia(a: ArtifactRow, r: ReleaseRow, path: string, ifNoneMatch: string | null): Response {98  if (r.state === "revoked") throw new ApiError(410, "release_revoked", `@${a.namespace}/${a.slug}@${r.version} was revoked: ${r.revocation_reason ?? "no reason recorded"}.`);99  if (!listed(r).some((e) => e.image === path)) throw notFound("Example image");100  const m = loadMedia(r).get(path);101  if (!m) throw notFound("Example image");102  const isPublic = a.visibility !== "private" && r.state === "published";103  const headers: Record<string, string> = {104    ETag: m.etag,105    "Cache-Control": isPublic ? "public, max-age=31536000, immutable" : "private, no-store",106    "X-Content-Type-Options": "nosniff",107    "Content-Security-Policy": "default-src 'none'; sandbox",108    "Content-Disposition": "inline",109    "Cross-Origin-Resource-Policy": "same-origin",110  };111  if (ifNoneMatch && ifNoneMatch.split(",").map((s) => s.trim()).includes(m.etag)) return new Response(null, { status: 304, headers });112  return new Response(new Uint8Array(m.data), { status: 200, headers: { ...headers, "Content-Type": m.mime, "Content-Length": String(m.data.length) } });113}114
Files main
Menu

Theme