* Allow ZIP uploads up to 5 GiB and stream them onto Docker computers Repository archives could not be picked at all: application/zip was not an allowed attachment type. ZIPs are now allowed with their own 5 GiB cap, upload as a stream, and reach the bot's Docker computer through a new streamed supervisor write that commits only after the body ended cleanly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ * Use a disallowed type for the invalid-attachment check now that ZIP is allowed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ --------- Co-authored-by: Claude <noreply@anthropic.com>
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import { t } from "@lingui/core/macro";
|
|
import {
|
|
ArtifactSchema,
|
|
ATTACHMENT_MAX_BYTES,
|
|
artifactMaxBytes,
|
|
attachmentKindOf,
|
|
} from "@rakazo/contracts";
|
|
import type { ArtifactTarget } from "./artifact-open";
|
|
import { rpc, withSpaceHeaders } from "./rpc";
|
|
|
|
type UploadedArtifact = ReturnType<typeof ArtifactSchema.parse>;
|
|
|
|
/** Largest file of this type the composer accepts. */
|
|
export function attachmentMaxBytes(mimeType: string): number {
|
|
return artifactMaxBytes(mimeType);
|
|
}
|
|
|
|
/** Media and anything above the inline RPC cap upload as a raw stream instead of base64. */
|
|
export function usesStreamingUpload(size: number, mimeType: string): boolean {
|
|
const kind = attachmentKindOf(mimeType);
|
|
return size > ATTACHMENT_MAX_BYTES || kind === "video" || kind === "audio";
|
|
}
|
|
|
|
export async function uploadAttachment(
|
|
target: ArtifactTarget,
|
|
file: File,
|
|
mimeType: string,
|
|
): Promise<UploadedArtifact> {
|
|
if (usesStreamingUpload(file.size, mimeType)) {
|
|
return uploadArtifactFile(target, file, file.name, mimeType);
|
|
}
|
|
const contentBase64 = await readFileAsBase64(file);
|
|
return rpc.artifacts.create({ ...target, name: file.name, mimeType, contentBase64 });
|
|
}
|
|
|
|
/** Uploads a file as a raw streamed body to the artifact route. */
|
|
export async function uploadArtifactFile(
|
|
target: ArtifactTarget,
|
|
file: Blob,
|
|
name: string,
|
|
mimeType: string,
|
|
): Promise<UploadedArtifact> {
|
|
const query = new URLSearchParams({ ...target, name });
|
|
const response = await fetch(`/api/artifacts?${query}`, {
|
|
method: "POST",
|
|
headers: withSpaceHeaders({ "content-type": mimeType }),
|
|
credentials: "include",
|
|
body: file,
|
|
});
|
|
const body: unknown = await response.json().catch(() => null);
|
|
const artifact = ArtifactSchema.safeParse(body);
|
|
if (response.ok && artifact.success) return artifact.data;
|
|
const error = (body as { error?: unknown } | null)?.error;
|
|
throw new Error(typeof error === "string" ? error : t`Could not upload ${name}. Try again.`);
|
|
}
|
|
|
|
function readFileAsBase64(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const result = typeof reader.result === "string" ? reader.result : "";
|
|
const base64 = result.includes(",") ? (result.split(",")[1] ?? "") : result;
|
|
resolve(base64);
|
|
};
|
|
reader.onerror = () => reject(reader.error ?? new Error("Failed to read file"));
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|