~james/core

core/web/src/lib/api/artifact.ts -rw-r--r-- 3.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import * as v from "@atcute/lexicons/validations";
import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts";
import { createCob, eraseCob, uploadBlobToKnot, type CobRef } from "#lib/api/cob.ts";
import { jsonGet } from "#lib/api/_request.ts";
import * as RepoArtifact from "#lib/api/lexicons/types/org/tangled/repo/artifact.ts";
import type * as ListArtifacts from "#lib/api/lexicons/types/org/tangled/repo/listArtifacts.ts";
import { datetimeOf, nsidOf, type Did, type KnotOrigin } from "#lib/api/syntax.ts";

export const ARTIFACT_COLLECTION = nsidOf("org.tangled.repo.artifact");
export const MAX_ARTIFACT_BYTES = 52_428_800;
export const MAX_ARTIFACT_NAME_LENGTH = 255;

export interface ArtifactSummary {
	uri: string;
	recordCid?: string;
	oid: string;
	name: string;
	blobCid: string;
	size: number;
	createdAt: string;
}

const GIT_OID_TYPE = "org.tangled.git.oid";

// the lexicon makes subject a union, thus the schema needs subject.$type
// the old artifact migration did not write $type into subject
// without this function, the schema refuses those records and the ui shows no artifacts
// remove this function when all records on the knots have subject.$type
const withSubjectType = (value: unknown): unknown => {
	if (typeof value !== "object" || value === null || !("subject" in value)) return value;
	const { subject } = value;
	if (typeof subject !== "object" || subject === null || "$type" in subject) return value;
	return { ...value, subject: { $type: GIT_OID_TYPE, ...subject } };
};

export const toArtifactSummary = ({
	uri,
	cid,
	value
}: ListArtifacts.ListItem): ArtifactSummary[] => {
	const parsed = v.safeParse(RepoArtifact.mainSchema, withSubjectType(value));
	if (!parsed.ok) return [];
	const { name, artifact, createdAt, subject } = parsed.value;
	return [
		{
			uri,
			recordCid: cid,
			oid: subject.oid,
			name,
			blobCid: artifact.ref.$link,
			size: artifact.size,
			createdAt
		}
	];
};

export const listArtifacts = async (
	ctx: BobbinContext,
	repoDid: Did,
	init?: XrpcRequestInit
): Promise<ArtifactSummary[]> => {
	const page = await jsonGet<ListArtifacts.$output>(
		ctx,
		"org.tangled.repo.listArtifacts",
		{ subject: repoDid, limit: 1000 },
		init
	);
	return page.items.flatMap(toArtifactSummary);
};

export const uploadArtifact = async (
	agent: OAuthUserAgent,
	knot: KnotOrigin,
	repo: Did,
	{ file, oid }: { file: File; oid: string }
): Promise<CobRef & { summary: ArtifactSummary }> => {
	if (file.size > MAX_ARTIFACT_BYTES) throw new Error("Artifacts can be at most 50 MB.");
	const blob = await uploadBlobToKnot(
		agent,
		knot,
		repo,
		await file.arrayBuffer(),
		file.type || "application/octet-stream"
	);
	const record: RepoArtifact.Main = {
		$type: "org.tangled.repo.artifact",
		subject: { $type: GIT_OID_TYPE, oid },
		name: file.name,
		artifact: blob,
		createdAt: datetimeOf(new Date().toISOString())
	};
	const ref = await createCob(agent, knot, repo, ARTIFACT_COLLECTION, record);
	return {
		...ref,
		summary: {
			uri: ref.uri,
			recordCid: ref.cid,
			oid,
			name: record.name,
			blobCid: blob.ref.$link,
			size: blob.size,
			createdAt: record.createdAt
		}
	};
};

export const deleteArtifact = (agent: OAuthUserAgent, knot: KnotOrigin, artifact: CobRef) =>
	eraseCob(agent, knot, artifact);