import { BadRequestException, Controller, Get, Param, Post, Query, Req, Res, UnauthorizedException, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { FilesService } from './files.service.js'; type AuthedRequest = { userId?: string }; type UploadedMulterFile = { originalname: string; mimetype: string; size: number; buffer: Buffer; }; @Controller('files') export class FilesController { constructor(private readonly files: FilesService) {} @Post() @UseInterceptors(FileInterceptor('file')) async upload( @Req() req: AuthedRequest, @UploadedFile() file: UploadedMulterFile | undefined, @Query('channelId') channelId?: string, ) { const userId = req.userId ?? ''; if (!userId) throw new UnauthorizedException('missing user'); if (!file || !file.buffer?.length) throw new BadRequestException('no file'); if (this.files.maxBytes > 0 && file.size > this.files.maxBytes) { throw new BadRequestException( `file exceeds limit of ${this.files.maxBytes} bytes`, ); } const row = await this.files.store({ channelId: channelId ? String(channelId) : null, uploaderUserId: userId, originalName: file.originalname || 'file', mimeType: file.mimetype, buffer: file.buffer, }); return { fileId: row.fileId, url: `/api/files/${row.fileId}`, name: row.originalName, mimeType: row.mimeType, size: Number(row.sizeBytes), expiresAt: row.expiresAt.toISOString(), }; } @Get(':fileId') async download( @Param('fileId') fileId: string, @Res() res: Response, ): Promise { const row = await this.files.find(fileId); if (!row) { res.status(404).json({ error: 'file_not_found' }); return; } const blob = await this.files.readBlob(row); const inline = /^(image|audio|video)\//.test(row.mimeType) || row.mimeType === 'application/pdf'; const safeName = row.originalName.replace(/["\r\n]/g, '_'); res.setHeader('Content-Type', row.mimeType); res.setHeader('Content-Length', String(blob.length)); res.setHeader( 'Content-Disposition', `${inline ? 'inline' : 'attachment'}; filename="${safeName}"`, ); res.setHeader('Cache-Control', 'private, max-age=3600'); res.end(blob); } }