26 File Upload und Storage

Die Verarbeitung von Datei-Uploads ist ein häufiger Anwendungsfall in modernen Webanwendungen. NestJS bietet durch die Integration mit Multer eine elegante Lösung für File Uploads und deren Verarbeitung.

26.1 Multer Integration

Multer ist die Standard-Middleware für die Behandlung von multipart/form-data in NestJS. Die Installation erfolgt über npm:

npm install @nestjs/platform-express multer
npm install @types/multer --save-dev

26.1.1 Grundlegendes File Upload

import { 
  Controller, 
  Post, 
  UploadedFile, 
  UseInterceptors 
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';

@Controller('upload')
export class UploadController {
  @Post('single')
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(@UploadedFile() file: Express.Multer.File) {
    return {
      filename: file.filename,
      originalname: file.originalname,
      size: file.size,
      mimetype: file.mimetype
    };
  }

  @Post('multiple')
  @UseInterceptors(FilesInterceptor('files', 10))
  uploadMultiple(@UploadedFiles() files: Express.Multer.File[]) {
    return files.map(file => ({
      filename: file.filename,
      size: file.size
    }));
  }
}

26.1.2 Multer-Konfiguration

import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';

@Module({
  imports: [
    MulterModule.register({
      storage: diskStorage({
        destination: './uploads',
        filename: (req, file, callback) => {
          const name = file.originalname.split('.')[0];
          const fileExtName = extname(file.originalname);
          const randomName = Array(4)
            .fill(null)
            .map(() => Math.round(Math.random() * 16).toString(16))
            .join('');
          callback(null, `${name}-${randomName}${fileExtName}`);
        },
      }),
      limits: {
        fileSize: 5 * 1024 * 1024, // 5MB
      },
    }),
  ],
  controllers: [UploadController],
})
export class UploadModule {}

26.2 File Validation

Die Validierung von hochgeladenen Dateien ist essentiell für die Sicherheit der Anwendung.

26.2.1 Custom File Validation Pipe

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class FileValidationPipe implements PipeTransform {
  private readonly allowedMimeTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf'
  ];

  private readonly maxFileSize = 5 * 1024 * 1024; // 5MB

  transform(file: Express.Multer.File): Express.Multer.File {
    if (!file) {
      throw new BadRequestException('Keine Datei hochgeladen');
    }

    if (!this.allowedMimeTypes.includes(file.mimetype)) {
      throw new BadRequestException(
        `Dateityp nicht erlaubt. Erlaubte Typen: ${this.allowedMimeTypes.join(', ')}`
      );
    }

    if (file.size > this.maxFileSize) {
      throw new BadRequestException(
        `Datei zu groß. Maximum: ${this.maxFileSize / 1024 / 1024}MB`
      );
    }

    return file;
  }
}

26.2.2 Verwendung der Validation Pipe

@Controller('upload')
export class UploadController {
  @Post('validated')
  @UseInterceptors(FileInterceptor('file'))
  uploadWithValidation(
    @UploadedFile(FileValidationPipe) file: Express.Multer.File
  ) {
    return { message: 'Datei erfolgreich hochgeladen', file: file.filename };
  }
}

26.3 Cloud Storage Integration

Für produktive Anwendungen ist oft Cloud Storage die bessere Wahl als lokaler Speicher.

26.3.1 AWS S3 Integration

npm install aws-sdk
import { Injectable } from '@nestjs/common';
import { S3 } from 'aws-sdk';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class S3Service {
  private s3: S3;

  constructor(private configService: ConfigService) {
    this.s3 = new S3({
      accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
      secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
      region: this.configService.get('AWS_REGION'),
    });
  }

  async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
    const uploadParams = {
      Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
      Key: key,
      Body: file.buffer,
      ContentType: file.mimetype,
      ACL: 'public-read',
    };

    const result = await this.s3.upload(uploadParams).promise();
    return result.Location;
  }

  async deleteFile(key: string): Promise<void> {
    await this.s3.deleteObject({
      Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
      Key: key,
    }).promise();
  }
}

26.3.2 Google Cloud Storage

npm install @google-cloud/storage
import { Injectable } from '@nestjs/common';
import { Storage } from '@google-cloud/storage';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GcsService {
  private storage: Storage;
  private bucketName: string;

  constructor(private configService: ConfigService) {
    this.storage = new Storage({
      projectId: this.configService.get('GCP_PROJECT_ID'),
      keyFilename: this.configService.get('GCP_KEY_FILE'),
    });
    this.bucketName = this.configService.get('GCS_BUCKET_NAME');
  }

  async uploadFile(file: Express.Multer.File, fileName: string): Promise<string> {
    const bucket = this.storage.bucket(this.bucketName);
    const fileUpload = bucket.file(fileName);

    const stream = fileUpload.createWriteStream({
      metadata: {
        contentType: file.mimetype,
      },
    });

    return new Promise((resolve, reject) => {
      stream.on('error', reject);
      stream.on('finish', () => {
        resolve(`gs://${this.bucketName}/${fileName}`);
      });
      stream.end(file.buffer);
    });
  }
}

26.4 File Streaming

Für große Dateien ist Streaming eine effiziente Lösung.

26.4.1 Download-Controller mit Streaming

import { Controller, Get, Param, Res, StreamableFile } from '@nestjs/common';
import { Response } from 'express';
import { createReadStream, existsSync } from 'fs';
import { join } from 'path';

@Controller('files')
export class FileController {
  @Get('download/:filename')
  downloadFile(@Param('filename') filename: string, @Res({ passthrough: true }) res: Response) {
    const filePath = join(process.cwd(), 'uploads', filename);
    
    if (!existsSync(filePath)) {
      throw new NotFoundException('Datei nicht gefunden');
    }

    const file = createReadStream(filePath);
    
    res.set({
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': `attachment; filename="${filename}"`,
    });

    return new StreamableFile(file);
  }

  @Get('view/:filename')
  viewFile(@Param('filename') filename: string): StreamableFile {
    const filePath = join(process.cwd(), 'uploads', filename);
    const file = createReadStream(filePath);
    return new StreamableFile(file);
  }
}

26.4.2 Upload-Streaming für große Dateien

import { Injectable } from '@nestjs/common';
import { createWriteStream } from 'fs';
import { join } from 'path';

@Injectable()
export class StreamUploadService {
  async uploadLargeFile(filename: string, stream: NodeJS.ReadableStream): Promise<void> {
    const uploadPath = join(process.cwd(), 'uploads', filename);
    const writeStream = createWriteStream(uploadPath);

    return new Promise((resolve, reject) => {
      stream.pipe(writeStream);
      writeStream.on('finish', resolve);
      writeStream.on('error', reject);
    });
  }
}

26.5 Image Processing

Für die Verarbeitung von Bildern kann Sharp verwendet werden.

npm install sharp
npm install @types/sharp --save-dev

26.5.1 Image Processing Service

import { Injectable } from '@nestjs/common';
import * as sharp from 'sharp';
import { join } from 'path';

@Injectable()
export class ImageProcessingService {
  async resizeImage(
    file: Express.Multer.File,
    width: number,
    height: number
  ): Promise<Buffer> {
    return await sharp(file.buffer)
      .resize(width, height, {
        fit: 'cover',
        position: 'center',
      })
      .jpeg({ quality: 80 })
      .toBuffer();
  }

  async createThumbnail(file: Express.Multer.File): Promise<Buffer> {
    return await sharp(file.buffer)
      .resize(200, 200)
      .jpeg({ quality: 70 })
      .toBuffer();
  }

  async optimizeImage(file: Express.Multer.File): Promise<Buffer> {
    const metadata = await sharp(file.buffer).metadata();
    
    if (metadata.format === 'jpeg') {
      return await sharp(file.buffer)
        .jpeg({ quality: 85, progressive: true })
        .toBuffer();
    } else if (metadata.format === 'png') {
      return await sharp(file.buffer)
        .png({ compressionLevel: 8 })
        .toBuffer();
    }
    
    return file.buffer;
  }
}

26.5.2 Integration in Controller

@Controller('images')
export class ImageController {
  constructor(private imageProcessingService: ImageProcessingService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('image'))
  async uploadImage(@UploadedFile() file: Express.Multer.File) {
    const optimized = await this.imageProcessingService.optimizeImage(file);
    const thumbnail = await this.imageProcessingService.createThumbnail(file);
    
    // Speichern der verarbeiteten Bilder
    // ... Implementierung abhängig vom Storage-System
    
    return {
      message: 'Bild erfolgreich hochgeladen und verarbeitet',
      originalSize: file.size,
      optimizedSize: optimized.length,
    };
  }
}

26.6 Best Practices

Sicherheit: Validieren Sie immer Dateitypen und -größen. Speichern Sie hochgeladene Dateien außerhalb des Web-Root-Verzeichnisses.

Performance: Nutzen Sie Streaming für große Dateien und implementieren Sie asynchrone Verarbeitung für zeitaufwändige Operationen.

Skalierbarkeit: Verwenden Sie Cloud Storage für produktive Anwendungen anstatt lokaler Dateispeicherung.

Monitoring: Überwachen Sie Upload-Volumes und Speicherverbrauch, um Ressourcen-Limits rechtzeitig zu erkennen.