11"use server" ;
22import { randomBytes } from "node:crypto" ;
3- import { createWriteStream } from "node:fs" ;
4- import { readFile , unlink } from "node:fs/promises" ;
3+ import { createReadStream , createWriteStream } from "node:fs" ;
4+ import { stat , unlink } from "node:fs/promises" ;
55import { tmpdir } from "node:os" ;
66import { join } from "node:path" ;
77import { ZipArchive } from "archiver" ;
@@ -22,6 +22,8 @@ const DATA_EXPORT_REDACTED_KEYS = new Set([
2222 "hcaRefreshToken" ,
2323 "inviteCode" ,
2424] ) ;
25+ const MAX_EXPORT_MEDIA_FILES = 5000 ;
26+ const MAX_EXPORT_MEDIA_BYTES = 20 * 1024 * 1024 * 1024 ;
2527
2628function redactDataExportValue ( key : string , value : unknown ) {
2729 if ( DATA_EXPORT_REDACTED_KEYS . has ( key ) ) return undefined ;
@@ -172,6 +174,18 @@ async function processDataExport(exportId: string, userId: string) {
172174 const jsonContent = JSON . stringify ( safeUserData , null , 2 ) ;
173175 archive . append ( jsonContent , { name : "user-data.json" } ) ;
174176 const mediaItems = userData . uploadedMedia || [ ] ;
177+ const totalMediaBytes = mediaItems . reduce (
178+ ( sum , item ) => sum + ( item . fileSize || 0 ) ,
179+ 0 ,
180+ ) ;
181+ if (
182+ mediaItems . length > MAX_EXPORT_MEDIA_FILES ||
183+ totalMediaBytes > MAX_EXPORT_MEDIA_BYTES
184+ ) {
185+ archive . abort ( ) ;
186+ await unlink ( tempPath ) . catch ( ( ) => { } ) ;
187+ throw new Error ( "Data export too large" ) ;
188+ }
175189 for ( const [ index , item ] of mediaItems . entries ( ) ) {
176190 if ( index % 5 === 0 ) {
177191 const currentExport = await db . query . dataExports . findFirst ( {
@@ -193,12 +207,11 @@ async function processDataExport(exportId: string, userId: string) {
193207 } ) ;
194208 const response = await s3Client . send ( command ) ;
195209 if ( response . Body ) {
196- const bytes = await response . Body . transformToByteArray ( ) ;
197210 const folder = item . mimeType . startsWith ( "image/" )
198211 ? "photos"
199212 : "videos" ;
200213 const zipPath = `media/${ folder } /${ safeFilename ( item . filename ) } ` ;
201- archive . append ( Buffer . from ( bytes ) , {
214+ archive . append ( response . Body as NodeJS . ReadableStream & any , {
202215 name : zipPath ,
203216 date :
204217 item . uploadedAt instanceof Date
@@ -217,14 +230,14 @@ async function processDataExport(exportId: string, userId: string) {
217230 archive . on ( "error" , reject ) ;
218231 } ) ;
219232 const s3Key = `exports/${ exportId } /archive.zip` ;
220- const fileBuffer = await readFile ( tempPath ) ;
233+ const fileStats = await stat ( tempPath ) ;
221234 await uploadToS3 (
222- fileBuffer ,
235+ createReadStream ( tempPath ) ,
223236 s3Key ,
224237 "application/zip" ,
225238 undefined ,
226239 undefined ,
227- fileBuffer . length ,
240+ fileStats . size ,
228241 ) ;
229242 try {
230243 await db
0 commit comments