`text_content` is an extracted-text cache, but nothing stopped it from
holding a dump of bytes:
- `fileToText` ended its switch with `default: return buffer.toString("utf-8")`
and defaulted a missing extension to "txt", so any binary was decoded.
- Both extraction callers gated on `media_type == "document"`, and
`attachmentFileKind` classes everything non-image/audio/video as a
document: an SVG, a zip, a spreadsheet, a .bin.
- Nothing capped the length, so a 20 MB attachment could leave 20 MB of
text in the database.
- `storeChatAttachments` copied the text onto a second `media` row, and
`SELECT m.*` pulled the column into page props and API responses.
On a real install an SVG held 6852 characters of markup and a
`.designcraft` file held 142 KB of raw bytes.
Changes:
- `file-to-text.ts` lists the text extensions explicitly and returns
undefined for anything else; no extension is no text.
- `can-extract-text.ts` is the one allowlist (reusing
`BotTextFileExtensions`), used by both upload routes and by
`uploadAndRecordMedia`; `extension-of.ts` is shared with the storage
catalog instead of duplicated.
- `BotAttachmentTextStoreMaxChars` (200k) caps the column via
`cap-text-content.ts`.
- `store-chat-attachments.ts` no longer copies the text; ingestion
re-reads the bytes once.
- `media-columns.ts` selects every media column but `text_content`, which
now has to be asked for (`grab-messages-media`'s reply path and
ingestion do); the media CRUD routes sanitize rows they write.
- `ingest-bot-file.ts` decides on size alone, since a cached slice made
an oversized file look already read.
- `bun run media:prune-text` cleans rows that were already stored
(rerunnable, with a VACUUM hint).
Verified on the dev database: both bad rows cleared (146 KB), rerun
reports nothing to do.
56 lines
1.5 KiB
TypeScript
56 lines
1.5 KiB
TypeScript
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
|
import decideTextContent from "@/src/utils/decide-text-content";
|
|
|
|
export type TextContentChange = {
|
|
media_id: number;
|
|
name: string;
|
|
before_chars: number;
|
|
after_chars: number;
|
|
/**
|
|
* What the row's `text_content` should become: `null` clears it.
|
|
*/
|
|
text_content: string | null;
|
|
};
|
|
|
|
type Params = {
|
|
/**
|
|
* The `media` rows that have any `text_content`.
|
|
*/
|
|
rows: Partial<BUN_SQLITE_COBBLE_MEDIA>[];
|
|
};
|
|
|
|
/**
|
|
* What `pruneMediaTextContent` would change, worked out without touching
|
|
* the database: one entry per row whose text belongs nowhere or is over
|
|
* the store ceiling, and nothing at all for a row that is already right.
|
|
*/
|
|
export default function planTextContentPrune({ rows }: Params): TextContentChange[] {
|
|
const changes: TextContentChange[] = [];
|
|
|
|
for (const row of rows) {
|
|
if (!row.id) continue;
|
|
|
|
const before = row.text_content || "";
|
|
|
|
if (!before.trim()) continue;
|
|
|
|
const text_content = decideTextContent({
|
|
name: row.title,
|
|
mime_type: row.mime_type,
|
|
text: before,
|
|
});
|
|
|
|
if (text_content === before) continue;
|
|
|
|
changes.push({
|
|
media_id: Number(row.id),
|
|
name: row.title || "no name",
|
|
before_chars: before.length,
|
|
after_chars: text_content?.length || 0,
|
|
text_content,
|
|
});
|
|
}
|
|
|
|
return changes;
|
|
}
|