File-Upload mit Nest.js
Als Standard-Verfahren für Datei-Uploads kommt bei Nest.js-Anwendungen typischerweise ein Datei-Upload als multipart/form-data
mit Hilfe der Multer-Bibliothek zum Einsatz.
Dies ist jedoch nicht immer passend:
Die Verwendung von multipart/form-data
kommt typischerweise im Kontext von HTML Formularen vor.
Ohne HTML Formular und eingebaute Logik eines Browser ist die Verwendung auf Clientseitig teilweise umständlich zu implementieren.
Im Kontext von Microservice Architekturen liegen manchmal Daten vor, die zwar als einzelne Verarbeitung behandelt werden sollen, die jedoch mit einem klassischen File-Upload Formular wenig zu tun haben.
Bei Nest.js kommt die aus dem Node / Express Umfeld bekannte Multer Bibliothek für Multipar-Uploads zum Einsatz. Mit einzelnen Binary-POSTs kann sie jedoch nicht umgehen.
In diesem Artikel werden wir einen Datei-Upload implementieren, der ohne Multer auskommt und stattdessen die Dateien direkt als Binärdaten einließt.
Nest.js Fileupload mit Multi-Part-Requests
Zum Vergleich der beiden Herangehensweisen gehen wir zunächst ein kurzes Beispiel für die herkömmliche Anbindung in Nest.js mit Multer durch.
Die von Nest.js direkt mitgelieferte Multer-Anbindung baut auf express auf und funktioniert so nur im Zusammenspiel mit der Nest.js-Express Anbindung, nicht jedoch mit dem alternativen Framework Fastify.
Dies ist auch daran zu erkennen, dass der zugehörige mitgelieferte FileInterceptor
aus dem Paket @nestjs/platform-express
stammt.
In folgendem Code-Ausschnitt ist zu sehen, wie dem FileInterceptor
der String foo
übergeben wird.
Dies bedeutet, dass sich die Datei, die hochgeladen werden soll, in über den Namen foo
referenziert wird:
Bei einem Multipart-Upload können ja gerade mehrere Dateien und Formularelemente gleichzeitig hochgeladen werden.
Mittels des @UploadedFile()
-Decorators kann die so erhaltene Datei dann als Parameter in die Request-Handler-Methode hineingereicht werden.
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class DemoController {
@Post('form')
@UseInterceptors(FileInterceptor('foo'))
uploadFileForm(@UploadedFile() file) {
console.log('form', file);
}
}
Da Dateien von Multer im Default in Memory gehalten werden, sollte bei großen Dateien zur Begrenzung des Ressourcen-Bedarfs direkt ins Dateisystem geschrieben werden.
Aus Sicherheitsaspekten sollte weiterhin eine generelle Größenbeschränkung für hochgeladene Dateien vorgegeben werden, damit die Anwendung nicht durch fehlerhafte oder absichtlich zu große Requests zum Absturz gebracht werden kann.
Beides ist beim Import des MulterModule
möglich und kann folgendermaßen aussehen:
MulterModule
MulterModule.register({
dest: './dist/uploads',
limits: {
fileSize: 50 * 1024 * 1024, // f.e. 50MB
},
}),
Um zu testen, dass die Anbindung so funktioniert, kann ein Request, z.B. mit httpie, gegen den Endpunkt ausgeführt werden:
.zip
-Datei.$ http --form localhost:3000/form foo@~/demo/demo-file.zip
Multer kann den Datei-Typ dabei anhand der Endung der jeweiligen Datei ermitteln.
Nest.js Fileupload als Request Body
Es kann vorkommen, dass der obige Ansatz nicht passend ist, zum Beispiel, wenn der Client die gewünschten Daten direkt im Request-Body sendet. Möchte man derartige Requests verarbeiten, muss direkt auf den Input des Requests zugegriffen werden. Dieser Ansatz wird im folgenden gezeigt.
Dazu ist es wichtig zu wissen, dass Nest.js im Default-Fall express
als HTTP-Adapter verwendet.
Nest.js bietet auch die Möglichkeit, auf die zugrundeliegende Express-Infrastruktur zuzugreifen:
Um sich die Express-Repräsentation des Request-Objektes zu besorgen, wird der @Req()
-Decorator verwendet.
Der Request ist dabei ein Node (Readable-)Stream
und damit kompatibel mit anderen Node Streams.
Es gibt im wesentlichen zwei APIs zum Umgang mit den Streams:
-
Per
.pipe()
mit einem Zielstream (z.B. Datei, Datenbankverbindung) verbinden, die Daten werden dann automatisch durchgereicht -
Mittels Event-Handler auf Aktivitäten reagieren
Event-basierte Request-Behandlung
Zunächst werfen wir einen Blick auf die explizite Event-Behandlung.
Der Express-Request bietet die Möglichkeit, direkte Event-Handler zu registrieren, mit dem einzelne Datenpakete empfangen und behandelt werden können.
Die folgenden Eventtypen gilt es zu berücksichtigen:
'data'
-
Ein Abschnitt der zu empfangenen Daten (bei
chunked
Encoding), oder alle Daten 'end'
-
Die Übertragung wurde beendet
'error'
-
Es ist ein Fehler aufgetreten
In folgendem Beispiel wird außerdem der HTTP Content-Length
-Header abgefragt.
Dieser kann genutzt werden, um z.B. eine Größenbeschränkung für hochgeladenen Dateien umzusetzen.
import { Controller, Headers, Post, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller()
export class DemoController {
@Post('events')
uploadFileStream(
@Headers('content-length') length: number,
@Req() req: Request
): void {
if(length > 50 * 1024 * 1024){
return;
}
let file = '';
req.on('data', (d: any) => {
file += d;
});
req.on('end', () => {
console.log('end', file);
});
}
}
Auch hier kann die Funktionalität wieder mit Hilfe eines HTTPie-Requests verifiziert werden.
Wichtig ist dabei, dass Nest.js bei einem Request mit Mimetype application/json
dass der Request JSON-Daten enthält und auch gleich versucht, die Daten nach JSON zu parsen.
Um dies zu verhindern und zu erreichen, dass der Request als Stream behandelt wird, muss ein Content-Type
-HTTP-Header gesetzt werden, der den Automatismus verhindert.
Zum Beispiel application/octet-stream
:
.zip
-Datei per Event-Stream.$ http localhost:3000/events Content-Type:application/octet-stream < ./demo/demo-file.zip
Request Daten mit .pipe()
Statt die Daten aus einzelnen Events zusammen zu setzen, können diese auch direkt als Stream behandelt werden.
Dazu muss man sich abermals den nativen Request besorgen.
Das Request-Objekt bietet dann die pipe()
-Methode an, mit der der Request als Stream eingelesen und direkt an einen weiteren Stream weitergeleitet werden kann.
In folgendem Beispiel wird der Stream zum Beispiel direkt in einen WriteStream
umgeleitet, der die hochgeladene Datei ins Dateisystem schreibt.
Generell kann der Datenstrom aus dem Request auf diese Weise an einen beliebigen Ziel-Stream weitergereicht werden.
import { Controller, Post, Req } from '@nestjs/common';
import { Request } from 'express';
import { createWriteStream, existsSync, mkdirSync } from 'fs';
@Controller()
export class DemoController {
@Post('stream')
uploadFileStream(@Req() req: Request): void {
if (!existsSync('./dist/stream')) {
mkdirSync('./dist/stream', { recursive: true });
}
const writeStream = createWriteStream(
'./dist/stream/upload' + Math.floor(Math.random() * 100),
);
req.pipe(writeStream);
}
}
Der Request zum Upload der Datei sieht dabei genau so aus, wie im Falle der Event-Basierten Verarbeitung:
.zip
-Datei per Stream.$ http localhost:3000/stream Content-Type:application/octet-stream < ./demo/demo-file.zip
Zu den Themen Nest.js und Node bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.