nsarrazin commited on
Commit
72becf3
·
1 Parent(s): 46e4f70

feat: add admin only export feature

Browse files
package-lock.json CHANGED
@@ -85,6 +85,7 @@
85
  "@types/parquetjs": "^0.10.3",
86
  "@types/sbd": "^1.0.5",
87
  "@types/uuid": "^9.0.8",
 
88
  "@typescript-eslint/eslint-plugin": "^6.x",
89
  "@typescript-eslint/parser": "^6.x",
90
  "bson-objectid": "^2.0.4",
@@ -115,7 +116,8 @@
115
  "unplugin-icons": "^0.16.1",
116
  "vite": "^6.3.5",
117
  "vite-node": "^3.0.9",
118
- "vitest": "^3.1.4"
 
119
  },
120
  "optionalDependencies": {
121
  "@anthropic-ai/sdk": "^0.32.1",
@@ -6804,6 +6806,16 @@
6804
  "@types/webidl-conversions": "*"
6805
  }
6806
  },
 
 
 
 
 
 
 
 
 
 
6807
  "node_modules/@typescript-eslint/eslint-plugin": {
6808
  "version": "6.21.0",
6809
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
@@ -18735,6 +18747,26 @@
18735
  "node": ">=12"
18736
  }
18737
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18738
  "node_modules/yn": {
18739
  "version": "3.1.1",
18740
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
 
85
  "@types/parquetjs": "^0.10.3",
86
  "@types/sbd": "^1.0.5",
87
  "@types/uuid": "^9.0.8",
88
+ "@types/yazl": "^3.3.0",
89
  "@typescript-eslint/eslint-plugin": "^6.x",
90
  "@typescript-eslint/parser": "^6.x",
91
  "bson-objectid": "^2.0.4",
 
116
  "unplugin-icons": "^0.16.1",
117
  "vite": "^6.3.5",
118
  "vite-node": "^3.0.9",
119
+ "vitest": "^3.1.4",
120
+ "yazl": "^3.3.1"
121
  },
122
  "optionalDependencies": {
123
  "@anthropic-ai/sdk": "^0.32.1",
 
6806
  "@types/webidl-conversions": "*"
6807
  }
6808
  },
6809
+ "node_modules/@types/yazl": {
6810
+ "version": "3.3.0",
6811
+ "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-3.3.0.tgz",
6812
+ "integrity": "sha512-mFL6lGkk2N5u5nIxpNV/K5LW3qVSbxhJrMxYGOOxZndWxMgCamr/iCsq/1t9kd8pEwhuNP91LC5qZm/qS9pOEw==",
6813
+ "dev": true,
6814
+ "license": "MIT",
6815
+ "dependencies": {
6816
+ "@types/node": "*"
6817
+ }
6818
+ },
6819
  "node_modules/@typescript-eslint/eslint-plugin": {
6820
  "version": "6.21.0",
6821
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
 
18747
  "node": ">=12"
18748
  }
18749
  },
18750
+ "node_modules/yazl": {
18751
+ "version": "3.3.1",
18752
+ "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz",
18753
+ "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==",
18754
+ "dev": true,
18755
+ "license": "MIT",
18756
+ "dependencies": {
18757
+ "buffer-crc32": "^1.0.0"
18758
+ }
18759
+ },
18760
+ "node_modules/yazl/node_modules/buffer-crc32": {
18761
+ "version": "1.0.0",
18762
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
18763
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
18764
+ "dev": true,
18765
+ "license": "MIT",
18766
+ "engines": {
18767
+ "node": ">=8.0.0"
18768
+ }
18769
+ },
18770
  "node_modules/yn": {
18771
  "version": "3.1.1",
18772
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
package.json CHANGED
@@ -41,6 +41,7 @@
41
  "@types/parquetjs": "^0.10.3",
42
  "@types/sbd": "^1.0.5",
43
  "@types/uuid": "^9.0.8",
 
44
  "@typescript-eslint/eslint-plugin": "^6.x",
45
  "@typescript-eslint/parser": "^6.x",
46
  "bson-objectid": "^2.0.4",
@@ -71,7 +72,8 @@
71
  "unplugin-icons": "^0.16.1",
72
  "vite": "^6.3.5",
73
  "vite-node": "^3.0.9",
74
- "vitest": "^3.1.4"
 
75
  },
76
  "type": "module",
77
  "dependencies": {
 
41
  "@types/parquetjs": "^0.10.3",
42
  "@types/sbd": "^1.0.5",
43
  "@types/uuid": "^9.0.8",
44
+ "@types/yazl": "^3.3.0",
45
  "@typescript-eslint/eslint-plugin": "^6.x",
46
  "@typescript-eslint/parser": "^6.x",
47
  "bson-objectid": "^2.0.4",
 
72
  "unplugin-icons": "^0.16.1",
73
  "vite": "^6.3.5",
74
  "vite-node": "^3.0.9",
75
+ "vitest": "^3.1.4",
76
+ "yazl": "^3.3.1"
77
  },
78
  "type": "module",
79
  "dependencies": {
src/lib/server/api/index.ts CHANGED
@@ -16,7 +16,11 @@ import superjson from "superjson";
16
  const prefix = `${base}/api/v2` as unknown as "";
17
 
18
  export const app = new Elysia({ prefix })
19
- .mapResponse(({ response }) => {
 
 
 
 
20
  return new Response(superjson.stringify(response), {
21
  headers: {
22
  "Content-Type": "application/json",
 
16
  const prefix = `${base}/api/v2` as unknown as "";
17
 
18
  export const app = new Elysia({ prefix })
19
+ .mapResponse(({ response, request }) => {
20
+ // Skip the /export endpoint
21
+ if (request.url.endsWith("/export")) {
22
+ return response as unknown as Response;
23
+ }
24
  return new Response(superjson.stringify(response), {
25
  headers: {
26
  "Content-Type": "application/json",
src/lib/server/api/routes/groups/misc.ts CHANGED
@@ -5,6 +5,9 @@ import { collections } from "$lib/server/database";
5
  import { authCondition } from "$lib/server/auth";
6
  import { config } from "$lib/server/config";
7
  import { Client } from "@gradio/client";
 
 
 
8
 
9
  export interface FeatureFlags {
10
  searchEnabled: boolean;
@@ -103,4 +106,133 @@ export const misc = new Elysia()
103
  } catch (e) {
104
  throw new Error("Error fetching space API. Is the name correct?");
105
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  });
 
5
  import { authCondition } from "$lib/server/auth";
6
  import { config } from "$lib/server/config";
7
  import { Client } from "@gradio/client";
8
+ import yazl from "yazl";
9
+ import { downloadFile } from "$lib/server/files/downloadFile";
10
+ import mimeTypes from "mime-types";
11
 
12
  export interface FeatureFlags {
13
  searchEnabled: boolean;
 
106
  } catch (e) {
107
  throw new Error("Error fetching space API. Is the name correct?");
108
  }
109
+ })
110
+ .get("/export", async ({ locals }) => {
111
+ if (!locals.user) {
112
+ throw new Error("Not logged in");
113
+ }
114
+
115
+ if (!locals.isAdmin) {
116
+ throw new Error("Not admin");
117
+ }
118
+
119
+ const zipfile = new yazl.ZipFile();
120
+
121
+ const promises = [
122
+ collections.conversations
123
+ .find({ ...authCondition(locals) })
124
+ .toArray()
125
+ .then(async (conversations) => {
126
+ const formattedConversations = await Promise.all(
127
+ conversations.map(async (conversation) => {
128
+ const hashes: string[] = [];
129
+ conversation.messages.forEach(async (message) => {
130
+ if (message.files) {
131
+ message.files.forEach((file) => {
132
+ hashes.push(file.value);
133
+ });
134
+ }
135
+ });
136
+ const files = await Promise.all(
137
+ hashes.map(async (hash) => {
138
+ const fileData = await downloadFile(hash, conversation._id);
139
+ return fileData;
140
+ })
141
+ );
142
+
143
+ const filenames: string[] = [];
144
+ files.forEach((file) => {
145
+ const extension = mimeTypes.extension(file.mime) || "bin";
146
+ const convId = conversation._id.toString();
147
+ const fileId = file.name.split("-")[1].slice(0, 8);
148
+ const fileName = `file-${convId}-${fileId}.${extension}`;
149
+ filenames.push(fileName);
150
+ zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName);
151
+ });
152
+
153
+ return {
154
+ ...conversation,
155
+ messages: conversation.messages.map((message) => {
156
+ return {
157
+ ...message,
158
+ files: filenames,
159
+ updates: undefined,
160
+ };
161
+ }),
162
+ };
163
+ })
164
+ );
165
+
166
+ zipfile.addBuffer(
167
+ Buffer.from(JSON.stringify(formattedConversations, null, 2)),
168
+ "conversations.json"
169
+ );
170
+ }),
171
+ collections.assistants
172
+ .find({ createdById: locals.user._id })
173
+ .toArray()
174
+ .then(async (assistants) => {
175
+ const formattedAssistants = await Promise.all(
176
+ assistants.map(async (assistant) => {
177
+ if (assistant.avatar) {
178
+ const fileId = collections.bucket.find({ filename: assistant._id.toString() });
179
+
180
+ const content = await fileId.next().then(async (file) => {
181
+ if (!file?._id) return;
182
+
183
+ const fileStream = collections.bucket.openDownloadStream(file?._id);
184
+
185
+ const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
186
+ const chunks: Uint8Array[] = [];
187
+ fileStream.on("data", (chunk) => chunks.push(chunk));
188
+ fileStream.on("error", reject);
189
+ fileStream.on("end", () => resolve(Buffer.concat(chunks)));
190
+ });
191
+
192
+ return fileBuffer;
193
+ });
194
+
195
+ if (!content) return;
196
+
197
+ zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`);
198
+ }
199
+
200
+ return {
201
+ _id: assistant._id.toString(),
202
+ name: assistant.name,
203
+ createdById: assistant.createdById.toString(),
204
+ createdByName: assistant.createdByName,
205
+ avatar: `avatar-${assistant._id.toString()}.jpg`,
206
+ modelId: assistant.modelId,
207
+ preprompt: assistant.preprompt,
208
+ description: assistant.description,
209
+ dynamicPrompt: assistant.dynamicPrompt,
210
+ exampleInputs: assistant.exampleInputs,
211
+ rag: assistant.rag,
212
+ tools: assistant.tools,
213
+ generateSettings: assistant.generateSettings,
214
+ createdAt: assistant.createdAt.toISOString(),
215
+ updatedAt: assistant.updatedAt.toISOString(),
216
+ };
217
+ })
218
+ );
219
+
220
+ zipfile.addBuffer(
221
+ Buffer.from(JSON.stringify(formattedAssistants, null, 2)),
222
+ "assistants.json"
223
+ );
224
+ }),
225
+ ];
226
+
227
+ await Promise.all(promises);
228
+
229
+ zipfile.end();
230
+
231
+ // @ts-expect-error - zipfile.outputStream is not typed correctly
232
+ return new Response(zipfile.outputStream, {
233
+ headers: {
234
+ "Content-Type": "application/zip",
235
+ "Content-Disposition": 'attachment; filename="export.zip"',
236
+ },
237
+ });
238
  });