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);