Skip to content

g1t/apps/web/app/lib/artifacts.server.ts

67 lines2,455 bytesCodeBlame
1import { env } from "cloudflare:workers";
2
3import type { RepoPath, Viewer } from "@g1t/contracts";
4
5import { actions } from "./services.server";
6
7/**
8 * A run's artifacts, for its page. The actions service lists them (their
9 * bytes are in R2, downloaded through the API's signed links); artifacts an
10 * older runner kept in KV (`a/{run}/{name}`, its bytes in chunks `…#0`,
11 * `…#1`) are listed too until KV expires them.
12 */
13export type ArtifactRow = {
14 /** The artifact's number; null for one kept in KV. */
15 id: number | null;
16 name: string;
17 size: number;
18 /** When it goes, RFC 3339; null for one kept in KV (14 days after it was made). */
19 expiresAt: string | null;
20 createdAt: string;
21};
22
23type Meta = { size: number; chunks: number; at: number; name: string };
24
25async function kvArtifacts(run: string): Promise<ArtifactRow[]> {
26 const listed = await env.BLOBS.list<Meta>({ prefix: `a/${run}/` });
27 return listed.keys
28 .filter((key) => !key.name.includes("#") && key.metadata)
29 .map((key) => ({
30 id: null,
31 name: key.metadata!.name,
32 size: key.metadata!.size,
33 expiresAt: new Date(key.metadata!.at + 14 * 86_400_000).toISOString(),
34 createdAt: new Date(key.metadata!.at).toISOString(),
35 }));
36}
37
38export async function listArtifacts(repo: RepoPath, viewer: Viewer, run: string): Promise<ArtifactRow[]> {
39 const [kept, legacy] = await Promise.all([
40 actions.artifacts(repo, viewer, { run, per_page: 100 }),
41 kvArtifacts(run).catch(() => []),
42 ]);
43 const rows: ArtifactRow[] = kept.ok
44 ? kept.value.artifacts.map((a) => ({ id: a.id, name: a.name, size: a.size, expiresAt: a.expires_at, createdAt: a.created_at }))
45 : [];
46 for (const row of legacy) {
47 if (!rows.some((r) => r.name === row.name)) rows.push(row);
48 }
49 return rows.sort((a, b) => a.name.localeCompare(b.name));
50}
51
52export async function readArtifact(run: string, name: string): Promise<Uint8Array | null> {
53 const base = `a/${run}/${name}`;
54 const meta = await env.BLOBS.get<Meta>(base, "json");
55 if (!meta) return null;
56 const parts = await Promise.all(
57 Array.from({ length: meta.chunks }, (_, index) => env.BLOBS.get(`${base}#${index}`, "arrayBuffer")),
58 );
59 if (parts.some((part) => part == null)) return null;
60 const out = new Uint8Array(meta.size);
61 let offset = 0;
62 for (const part of parts as ArrayBuffer[]) {
63 out.set(new Uint8Array(part), offset);
64 offset += part.byteLength;
65 }
66 return out;
67}