- Media rows can no longer be created through the CRUD API, updates only take description fields on the caller's own row, and every media read or delete goes through mediaFilePath, which refuses paths outside DATA_DIR or inside the database folder. - Uploads strip the file name and extension to safe characters and keep only title, description and summary from the request's media object. - Folder imports no longer follow symlinks (walkRegularFiles), and the source path is quoted in the computer's shell. - User and bot files are served with nosniff, and HTML, SVG, XML and scripts are sandboxed by CSP. - Sandbox cookies are kept to the computer's prefix, and websocket upgrades on /ws and the computer proxy must come from the app's own address (README and Help note nginx's Host header). - user_types CRUD writes go through the assignment matrix; members only read their own user record; read-only users can read CRUD again; grabUsers builds valid SQL for array search terms and bad pages; an admin media DELETE needs an id; read-only users can't delete media. - Rate limits and the audit log use the connection's address, trusting forwarded headers only from a proxy, and login limits are split per address, per account from that address and per account overall. Co-Authored-By: Claude Opus 5.5 <[email protected]>
70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
import mediaFilePath from "@/src/utils/media-file-path";
|
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
|
import type { BUN_SQLITE_COBBLE_BOT_FILES } from "@/db/types/db";
|
|
import { rmSync } from "node:fs";
|
|
import path from "node:path";
|
|
import AppError from "@/src/utils/app-error";
|
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
|
import type { StorageScope } from "@/src/utils/storage-access";
|
|
import grabBotFile from "./grab-bot-file";
|
|
import grabStorageDir from "./grab-storage-dir";
|
|
|
|
type Params = {
|
|
scopes: StorageScope[];
|
|
file_id: number | string;
|
|
};
|
|
|
|
/**
|
|
* Deletes a bot file: the catalog row (its chunks go with it), its bytes
|
|
* and its `media` row. Refuses anything outside the run's directories.
|
|
*/
|
|
export default async function deleteBotFile({ scopes, file_id }: Params) {
|
|
const file = await grabBotFile({ scopes, file_id });
|
|
|
|
if (!file?.id) {
|
|
throw new AppError(`That file isn't in your files.`);
|
|
}
|
|
|
|
const root = grabStorageDir({ scope: file.scope as any, owner_id: file.owner_id! });
|
|
const absolute = path.join(root, file.path || "");
|
|
|
|
if (absolute.startsWith(path.resolve(root))) {
|
|
rmSync(absolute, { force: true });
|
|
}
|
|
|
|
await BunSQLite.sql({ sql: `DELETE FROM bot_file_vectors WHERE file_id = ?`, values: [file.id] });
|
|
/**
|
|
* Deleted here rather than left to the schema's cascade: SQLite has
|
|
* foreign keys off per connection unless `enableForeignKeys` ran, and
|
|
* a script or CLI process that deletes a file would otherwise leave
|
|
* its chunks behind, which then fail the next schema sync.
|
|
*/
|
|
await BunSQLite.sql({ sql: `DELETE FROM bot_file_chunks WHERE file_id = ?`, values: [file.id] });
|
|
await BunSQLite.sql({ sql: `DELETE FROM bot_files WHERE id = ?`, values: [file.id] });
|
|
/**
|
|
* Its pending ingestion would only find nothing left.
|
|
*/
|
|
await BunSQLite.sql({
|
|
sql: `DELETE FROM file_ingest_queue WHERE file_id = ? AND status IN ('pending', 'processing')`,
|
|
values: [file.id],
|
|
}).catch(() => {});
|
|
|
|
if (file.media_id) {
|
|
const media = await BunSQLite.sql<{ media_write_path?: string; media_thumbnail_write_path?: string }>({
|
|
sql: `SELECT media_write_path, media_thumbnail_write_path FROM media WHERE id = ? LIMIT 1`,
|
|
values: [file.media_id],
|
|
}).then((res) => res.payload?.[0]);
|
|
|
|
for (const write_path of [media?.media_write_path, media?.media_thumbnail_write_path]) {
|
|
const absolute = mediaFilePath({ write_path });
|
|
|
|
if (absolute) rmSync(absolute, { force: true });
|
|
}
|
|
|
|
await BunSQLite.sql({ sql: `DELETE FROM media_paradigms WHERE media_id = ?`, values: [file.media_id] });
|
|
await BunSQLite.sql({ sql: `DELETE FROM media WHERE id = ?`, values: [file.media_id] });
|
|
}
|
|
|
|
return file as BUN_SQLITE_COBBLE_BOT_FILES;
|
|
}
|