/** * Records the images pasted into Markdown bodies, and binds each one to the * resource whose body links it. `routes/attachments.ts` stores and serves the * files; the services that save a body call {@link linkAttachments}. * * @module */ import { db, transaction } from "../db/connection.ts"; import { APIErrorCode } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; /** * Records attachment `id` once `POST /api/attachments` has written its file. * Until a saved body links it, the startup purge deletes it after * `ORPHANED_ATTACHMENTS_RETENTION_HOURS`. */ export function createAttachment(id: string, mime: string): void { db.prepare( "INSERT INTO attachments (id, mime, created_at) VALUES (?, ?, ?)", ).run(id, mime, new Date().toISOString()); } /** * Returns attachment `attachmentId`'s MIME type, for * `GET /api/attachments/:attachmentId` to serve its file with. * * @throws {APIException} 404 when there is no such attachment. */ export function getAttachment(attachmentId: string): { mime: string } { const row = db.prepare( "SELECT mime FROM attachments WHERE id = ?", ).get(attachmentId) as { mime: string } | undefined; if (!row) { throw new APIException( APIErrorCode.NOT_FOUND, 404, "Attachment not found", ); } return { mime: row.mime }; } const ATTACHMENT_RE = /\/api\/attachments\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g; /** * Binds the attachments whose URLs appear in `text` to `resourceId`, so the * startup purge keeps them. It is called whenever a dump, comment, chat * message, playlist or profile description is saved. An attachment already * bound stays with its resource, so pasting another's URL claims nothing, and * removing the link from a body never unbinds it. */ export function linkAttachments(text: string, resourceId: string): void { const ids = [...text.matchAll(ATTACHMENT_RE)].map((m) => m[1]); if (ids.length === 0) return; const update = db.prepare( "UPDATE attachments SET resource_id = ? WHERE id = ? AND resource_id IS NULL", ); transaction(() => { for (const id of ids) update.run(resourceId, id); }); }