Skip to content

Commit e83f9e2

Browse files
committed
feat: improve export feature
- add once per hour rate limit - remove await blocks, start streaming zip right away - add stats to logging
1 parent f9caa83 commit e83f9e2

File tree

4 files changed

+62
-9
lines changed

4 files changed

+62
-9
lines changed

src/lib/server/api/routes/groups/misc.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Client } from "@gradio/client";
88
import yazl from "yazl";
99
import { downloadFile } from "$lib/server/files/downloadFile";
1010
import mimeTypes from "mime-types";
11+
import { logger } from "$lib/server/logger";
1112

1213
export interface FeatureFlags {
1314
searchEnabled: boolean;
@@ -120,6 +121,32 @@ export const misc = new Elysia()
120121
throw new Error("Data export is not enabled");
121122
}
122123

124+
const nExports = await collections.messageEvents.countDocuments({
125+
userId: locals.user._id,
126+
type: "export",
127+
expiresAt: { $gt: new Date() },
128+
});
129+
130+
if (nExports >= 1) {
131+
throw new Error(
132+
"You have already exported your data recently. Please wait 1 hour before exporting again."
133+
);
134+
}
135+
136+
const stats: {
137+
nConversations: number;
138+
nMessages: number;
139+
nAssistants: number;
140+
nAvatars: number;
141+
nFiles: number;
142+
} = {
143+
nConversations: 0,
144+
nMessages: 0,
145+
nFiles: 0,
146+
nAssistants: 0,
147+
nAvatars: 0,
148+
};
149+
123150
const zipfile = new yazl.ZipFile();
124151

125152
const promises = [
@@ -129,8 +156,10 @@ export const misc = new Elysia()
129156
.then(async (conversations) => {
130157
const formattedConversations = await Promise.all(
131158
conversations.map(async (conversation) => {
159+
stats.nConversations++;
132160
const hashes: string[] = [];
133161
conversation.messages.forEach(async (message) => {
162+
stats.nMessages++;
134163
if (message.files) {
135164
message.files.forEach((file) => {
136165
hashes.push(file.value);
@@ -152,12 +181,13 @@ export const misc = new Elysia()
152181
files.forEach((file) => {
153182
if (!file) return;
154183

155-
const extension = mimeTypes.extension(file.mime) || "bin";
184+
const extension = mimeTypes.extension(file.mime) || null;
156185
const convId = conversation._id.toString();
157186
const fileId = file.name.split("-")[1].slice(0, 8);
158-
const fileName = `file-${convId}-${fileId}.${extension}`;
187+
const fileName = `file-${convId}-${fileId}` + (extension ? `.${extension}` : "");
159188
filenames.push(fileName);
160189
zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName);
190+
stats.nFiles++;
161191
});
162192

163193
return {
@@ -212,8 +242,11 @@ export const misc = new Elysia()
212242
if (!content) return;
213243

214244
zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`);
245+
stats.nAvatars++;
215246
}
216247

248+
stats.nAssistants++;
249+
217250
return {
218251
_id: assistant._id.toString(),
219252
name: assistant.name,
@@ -241,9 +274,24 @@ export const misc = new Elysia()
241274
}),
242275
];
243276

244-
await Promise.all(promises);
245-
246-
zipfile.end();
277+
Promise.all(promises).then(async () => {
278+
logger.info(
279+
{
280+
userId: locals.user?._id,
281+
...stats,
282+
},
283+
"Exported user data"
284+
);
285+
zipfile.end();
286+
if (locals.user?._id) {
287+
await collections.messageEvents.insertOne({
288+
userId: locals.user?._id,
289+
type: "export",
290+
createdAt: new Date(),
291+
expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
292+
});
293+
}
294+
});
247295

248296
// @ts-expect-error - zipfile.outputStream is not typed correctly
249297
return new Response(zipfile.outputStream, {

src/lib/server/database.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class Database {
242242
// No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users
243243
users.createIndex({ username: 1 }).catch((e) => logger.error(e));
244244
messageEvents
245-
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 })
245+
.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 1 })
246246
.catch((e) => logger.error(e));
247247
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch((e) => logger.error(e));
248248
sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e));

src/lib/types/MessageEvent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ import type { User } from "./User";
55
export interface MessageEvent extends Pick<Timestamps, "createdAt"> {
66
userId: User["_id"] | Session["sessionId"];
77
ip?: string;
8+
expiresAt: Date;
9+
type: "message" | "export";
810
}

src/routes/conversation/[id]/+server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
7474

7575
// register the event for ratelimiting
7676
await collections.messageEvents.insertOne({
77+
type: "message",
7778
userId,
7879
createdAt: new Date(),
80+
expiresAt: new Date(Date.now() + 60_000),
7981
ip: getClientAddress(),
8082
});
8183

@@ -103,17 +105,18 @@ export async function POST({ request, locals, params, getClientAddress }) {
103105
error(429, "Exceeded number of messages before login");
104106
}
105107
}
106-
107108
if (usageLimits?.messagesPerMinute) {
108109
// check if the user is rate limited
109110
const nEvents = Math.max(
110111
await collections.messageEvents.countDocuments({
111112
userId,
112-
createdAt: { $gte: new Date(Date.now() - 60_000) },
113+
type: "message",
114+
expiresAt: { $gt: new Date() },
113115
}),
114116
await collections.messageEvents.countDocuments({
115117
ip: getClientAddress(),
116-
createdAt: { $gte: new Date(Date.now() - 60_000) },
118+
type: "message",
119+
expiresAt: { $gt: new Date() },
117120
})
118121
);
119122
if (nEvents > usageLimits.messagesPerMinute) {

0 commit comments

Comments
 (0)