Skip to content

Commit fe229f3

Browse files
committed
refactor: prevent out-of-memory by streaming instead of awaiting
1 parent 93742b4 commit fe229f3

1 file changed

Lines changed: 50 additions & 38 deletions

File tree

apps/api/src/routes/exports/photos.archive.ts

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import archiver from 'archiver'
66
import type { Context } from 'hono'
77
import { stream } from 'hono/streaming'
88
import mime from 'mime'
9-
import { Readable } from 'node:stream'
109
import { z } from 'zod'
1110
import prisma from '../../prisma.js'
1211
import { openFileStream } from '../../services/file/helpers/getFileUrl.js'
@@ -119,54 +118,67 @@ export async function veranstaltungPhotoArchive(ctx: Context<{ Variables: Author
119118

120119
const zip = archiver('zip')
121120

122-
zip.on('warning', function (err) {
123-
if (err.code === 'ENOENT') {
124-
console.warn(err)
125-
} else {
126-
throw err
127-
}
128-
})
129-
130-
zip.on('error', function (err) {
131-
throw err
132-
})
133-
134121
ctx.status(201)
135122
ctx.header('Content-Type', 'application/zip')
136123
ctx.header('Content-Disposition', `attachment; filename="${mode === 'flat' ? 'FotosForAutomation' : 'Fotos'}.zip"`)
137124

138125
zip.append(`Gesamtzahl Fotos: ${anmeldungen.length}`, { name: `${baseDirectory}/README.txt` })
139126

140-
for (const { person, unterveranstaltung } of anmeldungen) {
141-
if (!person.photo) {
142-
continue
143-
}
127+
return stream(ctx, async (s) => {
128+
zip.on('data', (chunk) => {
129+
/* eslint-disable @typescript-eslint/no-floating-promises */
130+
s.write(chunk)
131+
})
144132

145-
const stream = await openFileStream(person.photo)
133+
for (const { person, unterveranstaltung } of anmeldungen) {
134+
if (!person.photo) {
135+
continue
136+
}
146137

147-
const directory = `${unterveranstaltung.veranstaltung.name}/${unterveranstaltung.gliederung.name}`
148-
const basename = mode === 'group' ? `${person.firstname} ${person.lastname}` : person.id
149-
const extension = mime.getExtension(person.photo.mimetype ?? 'text/plain')
138+
const stream = await openFileStream(person.photo)
150139

151-
zip.append(stream, {
152-
name:
153-
mode === 'group'
154-
? `${baseDirectory}/${directory}/${basename}.${extension}`
155-
: `Fotos/${person.photo.id}.${extension}`,
156-
date: person.photo.createdAt,
157-
})
158-
}
140+
stream.on('end', () => {
141+
stream.destroy()
142+
})
159143

160-
if (mode === 'flat') {
161-
const buffer = buildSheet(anmeldungen, account.person)
162-
zip.append(buffer, {
163-
name: 'Datenzusammenführung.xlsx',
164-
})
165-
}
144+
const directory = `${unterveranstaltung.veranstaltung.name}/${unterveranstaltung.gliederung.name}`
145+
const basename = mode === 'group' ? `${person.firstname} ${person.lastname}` : person.id
146+
const extension = mime.getExtension(person.photo.mimetype ?? 'text/plain')
147+
148+
zip.append(stream, {
149+
name:
150+
mode === 'group'
151+
? `${baseDirectory}/${directory}/${basename}.${extension}`
152+
: `Fotos/${person.photo.id}.${extension}`,
153+
date: person.photo.createdAt,
154+
})
155+
}
166156

167-
await zip.finalize()
157+
if (mode === 'flat') {
158+
const buffer = buildSheet(anmeldungen, account.person)
159+
zip.append(buffer, {
160+
name: 'Datenzusammenführung.xlsx',
161+
})
162+
}
168163

169-
return stream(ctx, async (s) => {
170-
await s.pipe(Readable.toWeb(zip))
164+
/* eslint-disable @typescript-eslint/no-floating-promises */
165+
zip.finalize()
166+
167+
await new Promise<void>((resolve, reject) => {
168+
zip.on('warning', function (err) {
169+
if (err.code === 'ENOENT') {
170+
console.warn(err)
171+
} else {
172+
reject(err)
173+
}
174+
})
175+
176+
zip.once('end', () => {
177+
resolve()
178+
})
179+
zip.once('error', (err) => {
180+
reject(err)
181+
})
182+
})
171183
})
172184
}

0 commit comments

Comments
 (0)