11const { 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
67const {
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
1718const 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