152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
import type { CreateFlowUploadPurpose } from "./createFlowUploadPurpose";
|
|
import {
|
|
CreateFlowUploadValidationError,
|
|
messageKeyForCreateFlowUploadReason,
|
|
validateCreateFlowUploadFile,
|
|
type CreateFlowUploadValidationReason,
|
|
} from "./createFlowUploadValidation";
|
|
|
|
export type UploadToServerResult = {
|
|
url: string;
|
|
id: string;
|
|
mimeType: string;
|
|
byteLength: number;
|
|
};
|
|
|
|
const VALIDATION_REASONS = new Set<CreateFlowUploadValidationReason>([
|
|
"empty",
|
|
"tooLarge",
|
|
"svg",
|
|
"invalidType",
|
|
"undecodable",
|
|
]);
|
|
|
|
function reasonFromUnknown(value: unknown): CreateFlowUploadValidationReason | null {
|
|
return typeof value === "string" && VALIDATION_REASONS.has(value as CreateFlowUploadValidationReason)
|
|
? (value as CreateFlowUploadValidationReason)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Authenticated multipart upload to `POST /api/uploads`.
|
|
* Caller must have a session cookie (same-origin fetch).
|
|
*/
|
|
export async function uploadCreateFlowFile(
|
|
file: File,
|
|
purpose: CreateFlowUploadPurpose,
|
|
): Promise<UploadToServerResult> {
|
|
const validated = await validateCreateFlowUploadFile(file, purpose);
|
|
if (validated.ok === false) {
|
|
throw new CreateFlowUploadValidationError(validated.reason);
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append("purpose", purpose);
|
|
formData.append("file", file);
|
|
|
|
const res = await fetch("/api/uploads", {
|
|
method: "POST",
|
|
body: formData,
|
|
credentials: "same-origin",
|
|
});
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await res.json();
|
|
} catch {
|
|
body = {};
|
|
}
|
|
|
|
const errParts = (() => {
|
|
if (body && typeof body === "object" && "error" in body) {
|
|
const e = (body as {
|
|
error?: { message?: string; code?: string };
|
|
details?: { reason?: unknown };
|
|
}).error;
|
|
const details = (body as { details?: { reason?: unknown } }).details;
|
|
const reason = reasonFromUnknown(details?.reason);
|
|
if (!e) {
|
|
return {
|
|
message: null as string | null,
|
|
code: null as string | null,
|
|
reason,
|
|
};
|
|
}
|
|
return {
|
|
message: typeof e.message === "string" ? e.message : null,
|
|
code: typeof e.code === "string" ? e.code : null,
|
|
reason,
|
|
};
|
|
}
|
|
return { message: null, code: null, reason: null };
|
|
})();
|
|
|
|
if (!res.ok) {
|
|
const fallback =
|
|
res.status === 413
|
|
? "FILE_TOO_LARGE"
|
|
: res.status === 401
|
|
? "UNAUTHORIZED"
|
|
: "UPLOAD_FAILED";
|
|
const code = errParts.code ?? errParts.message ?? fallback;
|
|
throw new UploadToServerError(res.status, code, errParts.reason);
|
|
}
|
|
|
|
const data = body as {
|
|
url?: string;
|
|
id?: string;
|
|
mimeType?: string;
|
|
byteLength?: number;
|
|
};
|
|
if (
|
|
typeof data.url !== "string" ||
|
|
typeof data.id !== "string" ||
|
|
typeof data.mimeType !== "string" ||
|
|
typeof data.byteLength !== "number"
|
|
) {
|
|
throw new UploadToServerError(res.status, "INVALID_RESPONSE");
|
|
}
|
|
|
|
return {
|
|
url: data.url,
|
|
id: data.id,
|
|
mimeType: data.mimeType,
|
|
byteLength: data.byteLength,
|
|
};
|
|
}
|
|
|
|
export class UploadToServerError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
readonly reason: CreateFlowUploadValidationReason | null;
|
|
|
|
constructor(
|
|
status: number,
|
|
code: string,
|
|
reason: CreateFlowUploadValidationReason | null = null,
|
|
) {
|
|
super(code);
|
|
this.name = "UploadToServerError";
|
|
this.status = status;
|
|
this.code = code;
|
|
this.reason = reason;
|
|
}
|
|
}
|
|
|
|
export function createFlowUploadFailureMessageKey(err: unknown): string {
|
|
if (err instanceof CreateFlowUploadValidationError) {
|
|
return messageKeyForCreateFlowUploadReason(err.reason);
|
|
}
|
|
if (err instanceof UploadToServerError) {
|
|
if (err.reason) return messageKeyForCreateFlowUploadReason(err.reason);
|
|
if (err.status === 413) return "errors.tooLarge";
|
|
if (err.status === 401) return "errors.unauthorized";
|
|
if (err.code === "server_misconfigured") {
|
|
return "errors.misconfigured";
|
|
}
|
|
}
|
|
return "errors.generic";
|
|
}
|
|
|
|
export { CreateFlowUploadValidationError };
|