Skip to content

Commit 0e71cd0

Browse files
committed
feat: update export worker to use getS3CompatibleStorage function, stream using PassThrough
1 parent 557ca22 commit 0e71cd0

3 files changed

Lines changed: 394 additions & 893 deletions

File tree

apps/consumer/src/workers/export.worker.js

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
const { Worker } = require('bullmq');
2-
const fs = require('fs');
3-
const path = require('path');
4-
const os = require('os');
2+
const { PassThrough } = require('stream');
3+
const { Upload } = require('@aws-sdk/lib-storage');
4+
const { GetObjectCommand } = require('@aws-sdk/client-s3');
5+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
56

67
const {
78
redis,
89
exportQueue,
910
emailQueue,
1011
Project,
1112
getConnection,
12-
getCompiledModel,
13-
getStorage,
14-
getBucket
13+
getCompiledModel,
14+
getS3CompatibleStorage,
15+
getBucket
1516
} = require('@urbackend/common');
1617

1718
const initExportWorker = () => {
@@ -24,68 +25,67 @@ const initExportWorker = () => {
2425

2526
const connection = await getConnection(projectId);
2627

27-
// stream to a local temp file first to prevent memory bloat on large db export
28-
const tempFilePath = path.join(os.tmpdir(), `export_${projectId}_${Date.now()}.json`);
29-
const writeStream = fs.createWriteStream(tempFilePath);
28+
console.log(`[ExportWorker] Preparing streaming upload to storage...`);
29+
30+
const { s3Client } = await getS3CompatibleStorage(project);
31+
const bucket = await getBucket(project);
32+
const storagePath = `${projectId}/exports/db_export_${Date.now()}.json`;
33+
34+
const passThrough = new PassThrough();
35+
36+
const upload = new Upload({
37+
client: s3Client,
38+
params: {
39+
Bucket: bucket,
40+
Key: storagePath,
41+
Body: passThrough,
42+
ContentType: 'application/json'
43+
}
44+
});
45+
46+
// Start the upload promise in parallel
47+
const uploadPromise = upload.done();
3048

3149
try {
32-
writeStream.write('{\n');
50+
passThrough.write('{\n');
3351

3452
for (let i = 0; i < project.collections.length; i++) {
3553
const col = project.collections[i];
3654
const Model = getCompiledModel(connection, col, projectId, project.resources.db.isExternal);
3755

38-
writeStream.write(` "${col.name}": [\n`);
56+
passThrough.write(` "${col.name}": [\n`);
3957

4058
// use a mongoose cursor to stream documents one by one
4159
const cursor = Model.find().lean().cursor();
4260
let first = true;
4361

4462
for await (const doc of cursor) {
45-
if (!first) writeStream.write(',\n');
46-
writeStream.write(` ${JSON.stringify(doc)}`);
63+
if (!first) passThrough.write(',\n');
64+
passThrough.write(` ${JSON.stringify(doc)}`);
4765
first = false;
4866
}
4967

50-
writeStream.write('\n ]');
51-
if (i < project.collections.length - 1) writeStream.write(',\n');
68+
passThrough.write('\n ]');
69+
if (i < project.collections.length - 1) passThrough.write(',\n');
5270
}
5371

54-
writeStream.write('\n}\n');
55-
writeStream.end();
56-
57-
await new Promise((resolve, reject) => {
58-
writeStream.on('finish', resolve);
59-
writeStream.on('error', reject);
60-
});
61-
62-
console.log(`[ExportWorker] Data written to temp file. Uploading to storage...`);
63-
64-
// upload to supabase / storage
65-
const supabase = await getStorage(project);
66-
const bucket = getBucket(project);
67-
const storagePath = `${projectId}/exports/db_export_${Date.now()}.json`;
68-
69-
const fileBuffer = fs.readFileSync(tempFilePath);
70-
const { error: uploadError } = await supabase.storage
71-
.from(bucket)
72-
.upload(storagePath, fileBuffer, { contentType: 'application/json', upsert: true });
72+
passThrough.write('\n}\n');
73+
passThrough.end();
7374

74-
if (uploadError) throw uploadError;
75+
console.log(`[ExportWorker] Database stream ended. Awaiting final storage upload chunks...`);
76+
await uploadPromise;
7577

7678
// create a signed URL valid for 24 hrs (86400 seconds)
77-
const { data: signedData, error: signedError } = await supabase.storage
78-
.from(bucket)
79-
.createSignedUrl(storagePath, 86400);
80-
81-
if (signedError) throw signedError;
79+
const command = new GetObjectCommand({ Bucket: bucket, Key: storagePath });
80+
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 86400 });
8281

8382
// queue the email to be sent to the user
84-
await emailQueue.add('send-export-email', { email, downloadUrl: signedData.signedUrl, projectName: project.name });
83+
await emailQueue.add('send-export-email', { email, downloadUrl: signedUrl, projectName: project.name });
8584
console.log(`[ExportWorker] Export completed! Email queued for ${email}`);
8685

87-
} finally {
88-
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
86+
} catch (error) {
87+
passThrough.destroy(error);
88+
throw error;
8989
}
9090
}, { connection: redis, concurrency: 2 });
9191

0 commit comments

Comments
 (0)