import { createHash, randomUUID } from 'node:crypto'; import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; const ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9-]{1,64}$/; const MIME_PATTERN = /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i; const DEFAULT_MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024; export interface CodingAttachmentRef { attachmentId: string; mime: string; byteLength: number; } export interface CodingAttachmentRecord extends CodingAttachmentRef { data: Buffer; } export interface CodingAttachmentStoreOptions { createId?: () => string; maxBytes?: number; } export class CodingAttachmentStore { private readonly createId: (() => string) | undefined; private readonly maxBytes: number; constructor( private readonly rootDir: string, options: CodingAttachmentStoreOptions = {}, ) { this.createId = options.createId; this.maxBytes = options.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES; } async put(data: Uint8Array, mime: string): Promise { const normalizedMime = mime.trim().toLowerCase(); if (!MIME_PATTERN.test(normalizedMime)) throw new Error('Attachment MIME type is invalid'); if (data.byteLength === 0 || data.byteLength > this.maxBytes) { throw new Error('Attachment size is invalid'); } // Live events and session hydration repeat the same large image. Reuse its // stored bytes, including the original upload, instead of writing a new copy. const attachmentId = this.createId?.() ?? createHash('sha256') .update(normalizedMime).update('\0').update(data).digest('hex'); if (!ATTACHMENT_ID_PATTERN.test(attachmentId)) throw new Error('Attachment id is invalid'); await mkdir(this.rootDir, { recursive: true }); const dataPath = path.join(this.rootDir, `${attachmentId}.bin`); const metadataPath = path.join(this.rootDir, `${attachmentId}.json`); if (!this.createId) { const existing = await stat(metadataPath).catch((error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') return null; throw error; }); if (existing) return { attachmentId, mime: normalizedMime, byteLength: data.byteLength }; } const temporaryDataPath = `${dataPath}.${randomUUID()}.tmp`; const temporaryMetadataPath = `${metadataPath}.${randomUUID()}.tmp`; const reference: CodingAttachmentRef = { attachmentId, mime: normalizedMime, byteLength: data.byteLength, }; await writeFile(temporaryDataPath, data); await writeFile(temporaryMetadataPath, `${JSON.stringify(reference)}\n`, 'utf8'); await rename(temporaryDataPath, dataPath); await rename(temporaryMetadataPath, metadataPath); return reference; } async read(attachmentId: string): Promise { if (!ATTACHMENT_ID_PATTERN.test(attachmentId)) throw new Error('Attachment id is invalid'); const metadata = JSON.parse(await readFile( path.join(this.rootDir, `${attachmentId}.json`), 'utf8', )) as CodingAttachmentRef; if (metadata.attachmentId !== attachmentId || !MIME_PATTERN.test(metadata.mime) || !Number.isSafeInteger(metadata.byteLength) || metadata.byteLength <= 0 || metadata.byteLength > this.maxBytes) { throw new Error('Attachment metadata is invalid'); } const data = await readFile(path.join(this.rootDir, `${attachmentId}.bin`)); if (data.byteLength !== metadata.byteLength) throw new Error('Attachment data is incomplete'); return { ...metadata, data }; } }