import { Readable } from "node:stream"; import { BadRequestException, ServiceUnavailableException } from "@nestjs/common"; import { OcisNotFoundError, OcisWebdavClient, ocisConfigFromEnv } from "../group-files/ocis-webdav.js"; /** Imports buffer the whole file in memory; 256 MiB is the v1 cap (streaming is a follow-up). */ export const IMPORT_MAX_BYTES = 256 * 1024 * 1024; export interface MxcUri { server: string; mediaId: string; } /** Strict mxc://server/mediaId (v3 media ids are opaque non-slash segments). */ export function parseMxcUri(raw: string): MxcUri { const match = /^mxc:\/\/([^/\s]+)\/([A-Za-z0-9_-]+)$/.exec(raw.trim()); if (match === null) throw new BadRequestException("mxcUri must look like mxc://server/mediaId"); return { server: match[1]!, mediaId: match[2]! }; } export async function bufferWithCap(stream: Readable, capBytes: number): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let total = 0; stream.on("data", (chunk: Buffer) => { total += chunk.length; if (total > capBytes) { stream.destroy(); reject(new BadRequestException(`file exceeds the ${Math.round(capBytes / (1024 * 1024))} MiB import cap`)); return; } chunks.push(chunk); }); stream.on("end", () => resolve(Buffer.concat(chunks))); stream.on("error", reject); }); } let ocisClient: OcisWebdavClient | null = null; /** Drive import: pull one oCIS resource through the service-account WebDAV client. */ export async function loadDriveBytes(resourceId: string): Promise<{ bytes: Buffer; mime: string }> { ocisClient ??= new OcisWebdavClient(ocisConfigFromEnv()); const download = await ocisClient.download(resourceId).catch((error: unknown) => { if (error instanceof OcisNotFoundError) throw new BadRequestException("no such Drive resource"); throw new ServiceUnavailableException("Drive is unreachable — try again later"); }); const bytes = await bufferWithCap(download.stream, IMPORT_MAX_BYTES); return { bytes, mime: download.contentType ?? "application/octet-stream" }; } export function matrixMediaConfig(): { homeserver: string; token: string } | null { const token = process.env.MATRIX_MEDIA_TOKEN?.trim() ?? ""; if (token === "") return null; const homeserver = (process.env.MATRIX_MEDIA_HOMESERVER_URL ?? process.env.ARCHIVE_PROVISIONER_HOMESERVER_URL ?? "").replace(/\/+$/, ""); if (homeserver === "") return null; return { homeserver, token }; } /** Chat import: pull one MXC from the homeserver's authenticated media repository. */ export async function loadMatrixBytes(mxc: MxcUri): Promise<{ bytes: Buffer; mime: string }> { const config = matrixMediaConfig(); if (config === null) throw new ServiceUnavailableException("chat media import is not configured: set MATRIX_MEDIA_TOKEN"); const encode = (value: string) => encodeURIComponent(value); const paths = [ `/_matrix/client/v1/media/download/${encode(mxc.server)}/${encode(mxc.mediaId)}`, `/_matrix/client/v3/media/download/${encode(mxc.server)}/${encode(mxc.mediaId)}`, ]; let lastStatus = 0; for (const path of paths) { const res = await fetch(`${config.homeserver}${path}`, { headers: { authorization: `Bearer ${config.token}`, accept: "application/octet-stream" }, redirect: "error", }).catch(() => null); if (res === null) throw new ServiceUnavailableException("chat homeserver unreachable — try again later"); if (res.status === 404 && path.startsWith("/_matrix/client/v1/")) { lastStatus = 404; continue; } if (!res.ok) { lastStatus = res.status; if (res.status === 404) throw new BadRequestException("no such chat media (or it has expired)"); throw new ServiceUnavailableException(`chat media download failed (HTTP ${String(lastStatus)})`); } if (res.body === null) throw new ServiceUnavailableException("chat media download returned an empty body"); const bytes = await bufferWithCap(Readable.fromWeb(res.body as import("node:stream/web").ReadableStream), IMPORT_MAX_BYTES); return { bytes, mime: res.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream" }; } throw new BadRequestException("no such chat media (or it has expired)"); }