Spaces:
Running
Running
| import { Elysia } from "elysia"; | |
| import { authPlugin } from "../../authPlugin"; | |
| import { loginEnabled } from "$lib/server/auth"; | |
| import { collections } from "$lib/server/database"; | |
| import { authCondition } from "$lib/server/auth"; | |
| import { config } from "$lib/server/config"; | |
| import yazl from "yazl"; | |
| import { downloadFile } from "$lib/server/files/downloadFile"; | |
| import mimeTypes from "mime-types"; | |
| import { logger } from "$lib/server/logger"; | |
| export interface FeatureFlags { | |
| enableAssistants: boolean; | |
| loginEnabled: boolean; | |
| isAdmin: boolean; | |
| } | |
| export const misc = new Elysia() | |
| .use(authPlugin) | |
| .get("/public-config", async () => config.getPublicConfig()) | |
| .get("/feature-flags", async ({ locals }) => { | |
| return { | |
| enableAssistants: config.ENABLE_ASSISTANTS === "true", | |
| loginEnabled, // login feature is on when OID is configured | |
| isAdmin: locals.isAdmin, | |
| } satisfies FeatureFlags; | |
| }) | |
| .get("/export", async ({ locals }) => { | |
| if (!locals.user) { | |
| throw new Error("Not logged in"); | |
| } | |
| if (!locals.isAdmin) { | |
| throw new Error("Not admin"); | |
| } | |
| if (config.ENABLE_DATA_EXPORT !== "true") { | |
| throw new Error("Data export is not enabled"); | |
| } | |
| const nExports = await collections.messageEvents.countDocuments({ | |
| userId: locals.user._id, | |
| type: "export", | |
| expiresAt: { $gt: new Date() }, | |
| }); | |
| if (nExports >= 1) { | |
| throw new Error( | |
| "You have already exported your data recently. Please wait 1 hour before exporting again." | |
| ); | |
| } | |
| const stats: { | |
| nConversations: number; | |
| nMessages: number; | |
| nFiles: number; | |
| nAssistants: number; | |
| nAvatars: number; | |
| } = { | |
| nConversations: 0, | |
| nMessages: 0, | |
| nFiles: 0, | |
| nAssistants: 0, | |
| nAvatars: 0, | |
| }; | |
| const zipfile = new yazl.ZipFile(); | |
| const promises = [ | |
| collections.conversations | |
| .find({ ...authCondition(locals) }) | |
| .toArray() | |
| .then(async (conversations) => { | |
| const formattedConversations = await Promise.all( | |
| conversations.map(async (conversation) => { | |
| stats.nConversations++; | |
| const hashes: string[] = []; | |
| conversation.messages.forEach(async (message) => { | |
| stats.nMessages++; | |
| if (message.files) { | |
| message.files.forEach((file) => { | |
| hashes.push(file.value); | |
| }); | |
| } | |
| }); | |
| const files = await Promise.all( | |
| hashes.map(async (hash) => { | |
| try { | |
| const fileData = await downloadFile(hash, conversation._id); | |
| return fileData; | |
| } catch { | |
| return null; | |
| } | |
| }) | |
| ); | |
| const filenames: string[] = []; | |
| files.forEach((file) => { | |
| if (!file) return; | |
| const extension = mimeTypes.extension(file.mime) || null; | |
| const convId = conversation._id.toString(); | |
| const fileId = file.name.split("-")[1].slice(0, 8); | |
| const fileName = `file-${convId}-${fileId}` + (extension ? `.${extension}` : ""); | |
| filenames.push(fileName); | |
| zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName); | |
| stats.nFiles++; | |
| }); | |
| return { | |
| ...conversation, | |
| messages: conversation.messages.map((message) => { | |
| return { | |
| ...message, | |
| files: filenames, | |
| updates: undefined, | |
| }; | |
| }), | |
| }; | |
| }) | |
| ); | |
| zipfile.addBuffer( | |
| Buffer.from(JSON.stringify(formattedConversations, null, 2)), | |
| "conversations.json" | |
| ); | |
| }), | |
| collections.assistants | |
| .find({ createdById: locals.user._id }) | |
| .toArray() | |
| .then(async (assistants) => { | |
| const formattedAssistants = await Promise.all( | |
| assistants.map(async (assistant) => { | |
| if (assistant.avatar) { | |
| const fileId = collections.bucket.find({ filename: assistant._id.toString() }); | |
| const content = await fileId.next().then(async (file) => { | |
| if (!file?._id) return; | |
| const fileStream = collections.bucket.openDownloadStream(file?._id); | |
| const fileBuffer = await new Promise<Buffer>((resolve, reject) => { | |
| const chunks: Uint8Array[] = []; | |
| fileStream.on("data", (chunk) => chunks.push(chunk)); | |
| fileStream.on("error", reject); | |
| fileStream.on("end", () => resolve(Buffer.concat(chunks))); | |
| }); | |
| return fileBuffer; | |
| }); | |
| if (!content) return; | |
| zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`); | |
| stats.nAvatars++; | |
| } | |
| stats.nAssistants++; | |
| return { | |
| _id: assistant._id.toString(), | |
| name: assistant.name, | |
| createdById: assistant.createdById.toString(), | |
| createdByName: assistant.createdByName, | |
| avatar: `avatar-${assistant._id.toString()}.jpg`, | |
| modelId: assistant.modelId, | |
| preprompt: assistant.preprompt, | |
| description: assistant.description, | |
| dynamicPrompt: assistant.dynamicPrompt, | |
| exampleInputs: assistant.exampleInputs, | |
| generateSettings: assistant.generateSettings, | |
| createdAt: assistant.createdAt.toISOString(), | |
| updatedAt: assistant.updatedAt.toISOString(), | |
| }; | |
| }) | |
| ); | |
| zipfile.addBuffer( | |
| Buffer.from(JSON.stringify(formattedAssistants, null, 2)), | |
| "assistants.json" | |
| ); | |
| }), | |
| ]; | |
| Promise.all(promises).then(async () => { | |
| logger.info( | |
| { | |
| userId: locals.user?._id, | |
| ...stats, | |
| }, | |
| "Exported user data" | |
| ); | |
| zipfile.end(); | |
| if (locals.user?._id) { | |
| await collections.messageEvents.insertOne({ | |
| userId: locals.user?._id, | |
| type: "export", | |
| createdAt: new Date(), | |
| expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour | |
| }); | |
| } | |
| }); | |
| // @ts-expect-error - zipfile.outputStream is not typed correctly | |
| return new Response(zipfile.outputStream, { | |
| headers: { | |
| "Content-Type": "application/zip", | |
| "Content-Disposition": 'attachment; filename="export.zip"', | |
| }, | |
| }); | |
| }); | |