import { randomUUID } from 'node:crypto'; import { mkdir, readFile, rename, 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; private readonly maxBytes: number; constructor( private readonly rootDir: string, options: CodingAttachmentStoreOptions = {}, ) { this.createId = options.createId ?? randomUUID; 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'); } const attachmentId = this.createId(); 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`); 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 }; } }