Files
lcbp3/backend/src/common/file-storage/file-storage.controller.ts
T

118 lines
4.1 KiB
TypeScript

// File: src/common/file-storage/file-storage.controller.ts
import {
Controller,
Post,
Get,
Delete, // ✅ Import Delete
Param,
UseInterceptors,
UploadedFile,
UseGuards,
Request,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator,
Res,
StreamableFile,
ParseIntPipe,
} from '@nestjs/common';
import type { Response } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileStorageService } from './file-storage.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RbacGuard } from '../guards/rbac.guard';
import { RequirePermission } from '../decorators/require-permission.decorator';
import type { RequestWithUser } from '../interfaces/request-with-user.interface';
@Controller('files')
@UseGuards(JwtAuthGuard)
export class FileStorageController {
constructor(private readonly fileStorageService: FileStorageService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file')) // รับ field ชื่อ 'file'
async uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 50 * 1024 * 1024 }), // 50MB
// ตรวจสอบประเภทไฟล์ (Regex) - รวม image, pdf, docs, zip
new FileTypeValidator({
fileType:
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
}),
],
})
)
file: Express.Multer.File,
@Request() req: RequestWithUser
) {
// ส่ง userId จาก Token ไปด้วย
return this.fileStorageService.upload(file, req.user.user_id);
}
/**
* Endpoint สำหรับดาวน์โหลดไฟล์
* GET /files/:id/download
*/
@Get(':id/download')
async downloadFile(
@Param('id', ParseIntPipe) id: number,
@Res({ passthrough: true }) res: Response
): Promise<StreamableFile> {
const { stream, attachment } = await this.fileStorageService.download(id);
// Encode ชื่อไฟล์เพื่อรองรับภาษาไทยและตัวอักษรพิเศษใน Header
const encodedFilename = encodeURIComponent(attachment.originalFilename);
res.set({
'Content-Type': attachment.mimeType,
// บังคับให้ browser ดาวน์โหลดไฟล์ แทนการ preview
'Content-Disposition': `attachment; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`,
'Content-Length': attachment.fileSize,
});
return new StreamableFile(stream);
}
/**
* ADR-021: Preview Endpoint — GET /files/preview/:publicId
* ส่งไฟล์กลับพร้อม Content-Disposition: inline เพื่อให้ Browser แสดงผลโดยตรง
* ใช้ publicId (UUIDv7) ตาม ADR-019 — ไม่ใช้ INT id
*/
@Get('preview/:publicId')
@UseGuards(RbacGuard)
@RequirePermission('document.view')
async previewFile(
@Param('publicId') publicId: string,
@Res({ passthrough: true }) res: Response
): Promise<StreamableFile> {
const { stream, attachment } =
await this.fileStorageService.preview(publicId);
const encodedFilename = encodeURIComponent(attachment.originalFilename);
res.set({
'Content-Type': attachment.mimeType ?? 'application/octet-stream',
// inline = browser แสดงผล, attachment = บังคับดาวน์โหลด
'Content-Disposition': `inline; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`,
'Content-Length': String(attachment.fileSize),
});
return new StreamableFile(stream);
}
/**
* ✅ NEW: Delete Endpoint
* DELETE /files/:id
*/
@Delete(':id')
async deleteFile(
@Param('id', ParseIntPipe) id: number,
@Request() req: RequestWithUser
) {
// ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ
await this.fileStorageService.delete(id, req.user.user_id);
return { message: 'File deleted successfully', id };
}
}