| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import {
- BadRequestException,
- Body,
- Controller,
- Post,
- Request,
- UploadedFile,
- UseGuards,
- UseInterceptors,
- Logger,
- } from '@nestjs/common';
- import { FileInterceptor } from '@nestjs/platform-express';
- import { UploadService } from './upload.service';
- import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
- import { CombinedAuthGuard } from '../auth/combined-auth.guard';
- import { RolesGuard } from '../auth/roles.guard';
- import { Roles } from '../auth/roles.decorator';
- import { UserRole } from '../user/user-role.enum';
- import { errorMessages } from '../i18n/messages';
- import {
- DEFAULT_CHUNK_SIZE,
- MIN_CHUNK_SIZE,
- MAX_CHUNK_SIZE,
- DEFAULT_CHUNK_OVERLAP,
- DEFAULT_MAX_OVERLAP_RATIO,
- MAX_FILE_SIZE,
- DEFAULT_LANGUAGE
- } from '../common/constants';
- import { I18nService } from '../i18n/i18n.service';
- import { isAllowedByExtension, IMAGE_MIME_TYPES } from '../common/file-support.constants';
- export interface UploadConfigDto {
- chunkSize?: string;
- chunkOverlap?: string;
- embeddingModelId?: string;
- mode?: 'fast' | 'precise'; // 処理モード
- }
- @Controller('upload')
- @UseGuards(CombinedAuthGuard, RolesGuard)
- export class UploadController {
- private readonly logger = new Logger(UploadController.name);
- constructor(
- private readonly uploadService: UploadService,
- private readonly knowledgeBaseService: KnowledgeBaseService,
- private readonly i18nService: I18nService,
- ) { }
- @Post()
- @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
- @UseInterceptors(
- FileInterceptor('file', {
- fileFilter: (req, file, cb) => {
- // 画像MIMEタイプまたは拡張子によるチェック
- const isAllowed = IMAGE_MIME_TYPES.includes(file.mimetype) ||
- isAllowedByExtension(file.originalname);
- if (isAllowed) {
- cb(null, true);
- } else {
- cb(
- new BadRequestException(
- (errorMessages[DEFAULT_LANGUAGE]?.uploadTypeUnsupported || errorMessages['ja'].uploadTypeUnsupported).replace('{type}', file?.mimetype ?? 'unknown')
- ),
- false,
- );
- }
- },
- }),
- )
- async uploadFile(
- @UploadedFile() file: Express.Multer.File,
- @Request() req,
- @Body() config: UploadConfigDto,
- ) {
- if (!file) {
- throw new BadRequestException(this.i18nService.getMessage('uploadNoFile'));
- }
- // ファイルサイズの検証(フロントエンド制限 + バックエンド検証)
- const maxFileSize = parseInt(
- process.env.MAX_FILE_SIZE || String(MAX_FILE_SIZE),
- ); // 100MB
- if (file.size > maxFileSize) {
- throw new BadRequestException(
- this.i18nService.formatMessage('uploadSizeExceeded', { size: this.formatBytes(file.size), max: this.formatBytes(maxFileSize) }),
- );
- }
- // 埋め込みモデル設定の検証
- if (!config.embeddingModelId) {
- throw new BadRequestException(this.i18nService.getMessage('uploadModelRequired'));
- }
- this.logger.log(
- `ユーザー ${req.user.id} がファイルをアップロードしました: ${file.originalname} (${this.formatBytes(file.size)})`,
- );
- const fileInfo = await this.uploadService.processUploadedFile(file);
- // 設定パラメータを解析し、安全なデフォルト値を設定
- const indexingConfig = {
- chunkSize: config.chunkSize ? parseInt(config.chunkSize) : DEFAULT_CHUNK_SIZE,
- chunkOverlap: config.chunkOverlap ? parseInt(config.chunkOverlap) : DEFAULT_CHUNK_OVERLAP,
- embeddingModelId: config.embeddingModelId || null,
- };
- // オーバーラップサイズがチャンクサイズの50%を超えないようにする
- if (indexingConfig.chunkOverlap > indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO) {
- indexingConfig.chunkOverlap = Math.floor(indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO);
- this.logger.warn(
- this.i18nService.formatMessage('overlapAdjusted', { newSize: indexingConfig.chunkOverlap }),
- );
- }
- // データベースに保存し、インデックスプロセスをトリガー(非同期)
- const kb = await this.knowledgeBaseService.createAndIndex(
- fileInfo,
- req.user.id,
- req.user.tenantId, // Ensure tenantId is passed down
- {
- ...indexingConfig,
- mode: config.mode || 'fast',
- } as any, // Bypass strict type check for now or cast to correct type
- );
- return {
- message: this.i18nService.getMessage('uploadSuccess'),
- id: kb.id,
- filename: fileInfo.filename,
- originalname: fileInfo.originalname,
- status: kb.status,
- mode: config.mode || 'fast',
- config: indexingConfig,
- estimatedChunks: Math.ceil(file.size / (indexingConfig.chunkSize * 4)), // 推定チャンク数
- };
- }
- @Post('text')
- @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
- async uploadText(
- @Request() req,
- @Body() body: {
- content: string;
- title: string;
- chunkSize?: string;
- chunkOverlap?: string;
- embeddingModelId?: string;
- mode?: 'fast' | 'precise';
- }
- ) {
- if (!body.content || !body.title) {
- throw new BadRequestException(this.i18nService.getMessage('contentAndTitleRequired'));
- }
- const fs = await import('fs');
- const path = await import('path');
- const uploadPath = process.env.UPLOAD_FILE_PATH || './uploads';
- if (!fs.existsSync(uploadPath)) {
- fs.mkdirSync(uploadPath, { recursive: true });
- }
- const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
- const filename = `note-${uniqueSuffix}.md`;
- const filePath = path.join(uploadPath, filename);
- // Save content to file (mocking a file upload)
- fs.writeFileSync(filePath, body.content, 'utf8');
- const fileSize = Buffer.byteLength(body.content, 'utf8');
- // Mimic Multer file info
- const fileInfo = {
- filename: filename,
- originalname: body.title.endsWith('.md') ? body.title : `${body.title}.md`,
- size: fileSize,
- mimetype: 'text/markdown',
- path: filePath
- };
- // Validating Config
- if (!body.embeddingModelId) {
- throw new BadRequestException(this.i18nService.getMessage('uploadModelRequired'));
- }
- const indexingConfig = {
- chunkSize: body.chunkSize ? parseInt(body.chunkSize) : DEFAULT_CHUNK_SIZE,
- chunkOverlap: body.chunkOverlap ? parseInt(body.chunkOverlap) : DEFAULT_CHUNK_OVERLAP,
- embeddingModelId: body.embeddingModelId || null,
- };
- if (indexingConfig.chunkOverlap > indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO) {
- indexingConfig.chunkOverlap = Math.floor(indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO);
- }
- const kb = await this.knowledgeBaseService.createAndIndex(
- fileInfo,
- req.user.id,
- req.user.tenantId,
- {
- ...indexingConfig,
- mode: body.mode || 'fast',
- } as any
- );
- return {
- message: this.i18nService.getMessage('uploadTextSuccess'),
- id: kb.id,
- filename: fileInfo.filename,
- originalname: fileInfo.originalname,
- status: kb.status,
- config: indexingConfig
- };
- }
- private formatBytes(bytes: number): string {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
- }
|