Files
cobble/src/pages/api/admin/upload-media.ts
T
tbenandClaude Opus 5.5 ac3ebc43d4 Keep searchable text only in knowledgebases
Every bot file was chunked into bot_file_chunks and embedded into
bot_file_vectors, and uploads kept their extracted text in
media.text_content, which together took most of the database. Bot
files are no longer indexed: bots find them by name or path with
list_files and read them with read_file, and search_files, ingest_file
and the files modal's re-read button are gone. Attachments keep no
text; a reply reads a document from the file. Only knowledgebase
documents keep text.

The schema drops both tables and the ingest columns. At boot,
dropStoredFileText removes what older versions stored (file jobs,
upload text) and vacuums once much of the database is free. The
media:prune-text script it replaces is removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 04:44:46 +01:00

125 lines
3.6 KiB
TypeScript

import twuiNumberfy from "@moduletrace/twui/utils/numberfy";
import loginUser from "@/src/functions/backend/auth/login-user";
import userAuth from "@/src/functions/backend/auth/user-auth";
import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media";
import type { ApiReqParams } from "@/src/types";
import bufferFromBase64 from "@/src/utils/buffer-from-base-64";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import _ from "lodash";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import canUpdateUser from "@/src/utils/rbac/can-update-user";
import grabUserTypes from "@/src/functions/backend/db/users/grab-user-types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
const req = params.req;
const body = params.body as ApiReqParams;
if (req.method !== "POST") {
return {
success: false,
};
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
try {
const {
user_id,
media_base_64_data_url,
media_paradigms,
media_type,
is_media_primary,
update_on_duplicate,
reauth,
media,
media_name,
} = body;
/**
* Media is owned by the logged in user unless an actor with
* permission to update the target user uploads on their behalf.
*/
const target_user_id = user_id ? twuiNumberfy(user_id) : user.id;
const is_self = String(target_user_id) === String(user.id);
/**
* `read_only` users cannot modify records, but they may still manage
* their own profile image.
*/
if (isReadOnlyUser({ user_types })) {
const is_own_profile_image =
is_self && media_paradigms?.includes("user-profile-image");
if (!is_own_profile_image) {
throw new Error(`This user can only read records.`);
}
}
if (!is_self) {
const target_user_types = await grabUserTypes({
user_id: target_user_id,
});
const can_update_target = canUpdateUser({
actor_user_types: user_types,
actor_user_id: user.id,
target_user_id,
target_user_types,
});
if (!can_update_target) {
throw new Error(`Operation not allowed!`);
}
}
if (!media_base_64_data_url) {
throw new Error(
`No image payload sent! Base64 data URL with mime type is required.`,
);
}
const { buffer, ext } = await bufferFromBase64(media_base_64_data_url);
const new_media = await uploadAndRecordMedia({
data: buffer,
media_paradigms,
media_type,
is_primary: is_media_primary,
update_on_duplicate,
media,
media_mime_type: ext,
user: { id: target_user_id },
media_name,
});
if (reauth) {
return await loginUser({
user_id: Number(user.id),
req,
});
}
return {
...new_media,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};