66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
}
|