You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

105 lines
3.5 KiB

import { fetchPopiserverGateway as fetch } from "@/app/api/_popiserverFetch";
import { NextRequest } from "next/server";
import { ApiError } from "./_errors";
import { getPopiartApiBaseUrl } from "./_popiartBaseUrl";
const POPI_MEDIA_UPLOAD_PATH = "/api_client/media/upload";
type PopiserverResponse<T> = {
status?: string;
message?: string;
data?: T;
};
type PopiMedia = {
path?: string;
url?: string;
originPath?: string;
originUrl?: string;
};
function getPopiBaseUrl(request: NextRequest): string {
void request;
return getPopiartApiBaseUrl();
}
function parseDataUrl(value: string): { mimeType: string; data: Buffer } | null {
const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/);
if (!match) return null;
return {
mimeType: match[1] || "application/octet-stream",
data: Buffer.from(match[2], "base64"),
};
}
function extensionForMimeType(mimeType: string): string {
const normalized = mimeType.toLowerCase();
if (normalized === "image/jpeg") return "jpg";
if (normalized === "image/png") return "png";
if (normalized === "image/webp") return "webp";
if (normalized === "image/gif") return "gif";
if (normalized === "video/mp4") return "mp4";
if (normalized === "video/webm") return "webm";
if (normalized === "video/quicktime") return "mov";
if (normalized === "audio/mpeg" || normalized === "audio/mp3") return "mp3";
if (normalized === "audio/wav" || normalized === "audio/x-wav") return "wav";
if (normalized === "audio/ogg") return "ogg";
if (normalized === "audio/flac") return "flac";
if (normalized === "audio/aac") return "aac";
if (normalized === "model/gltf-binary") return "glb";
return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin";
}
function mediaResourceUrl(media: PopiMedia): string | null {
return media.url || media.originUrl || media.path || media.originPath || null;
}
export function isDataUrl(value: string | null | undefined): value is string {
return typeof value === "string" && value.startsWith("data:");
}
export async function uploadDataUrlToPopiMedia(
request: NextRequest,
token: string,
dataUrl: string,
filePrefix: string
): Promise<string> {
const parsed = parseDataUrl(dataUrl);
if (!parsed) {
throw ApiError.badRequest("Invalid media data URL.");
}
const extension = extensionForMimeType(parsed.mimeType);
const arrayBuffer = parsed.data.buffer.slice(
parsed.data.byteOffset,
parsed.data.byteOffset + parsed.data.byteLength
) as ArrayBuffer;
const blob = new Blob([arrayBuffer], { type: parsed.mimeType });
const formData = new FormData();
formData.append("file", blob, `${filePrefix}-${Date.now()}.${extension}`);
const upstream = await fetch(getPopiBaseUrl(request) + POPI_MEDIA_UPLOAD_PATH, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
token,
},
body: formData,
cache: "no-store",
});
const payload = (await upstream.json().catch(() => null)) as PopiserverResponse<PopiMedia> | null;
if (!upstream.ok) {
throw ApiError.provider(payload?.message || "Popiserver media upload failed.", upstream.status);
}
if (!payload || payload.status !== "0000" || !payload.data) {
throw ApiError.provider(payload?.message || "Popiserver media upload returned an error.", 502);
}
const url = mediaResourceUrl(payload.data);
if (!url) {
throw ApiError.provider("Popiserver media upload response is missing resource URL.", 502);
}
return url;
}