diff --git a/core/src/main/java/com/opencontext/controller/DocsSourceController.java b/core/src/main/java/com/opencontext/controller/DocsSourceController.java index 7511e4e..86abefe 100644 --- a/core/src/main/java/com/opencontext/controller/DocsSourceController.java +++ b/core/src/main/java/com/opencontext/controller/DocsSourceController.java @@ -46,9 +46,12 @@ public interface DocsSourceController { **Authentication Required:** X-API-KEY header must be provided. **Supported File Types:** - - PDF documents (application/pdf) - - Markdown files (text/markdown) - - Plain text files (text/plain) + - PDF documents (application/pdf, .pdf files) + - Markdown files (text/markdown, .md or .markdown files) + - Plain text files (text/plain, .txt files) + + **Note:** The system automatically detects file types based on both the Content-Type header + and file extension. the system will properly recognize Markdown files regardless of how the browser sends the Content-Type. **File Size Limit:** 100MB @@ -60,9 +63,9 @@ public interface DocsSourceController { required = true, content = @Content( mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, - examples = @ExampleObject( - name = "Upload a Markdown file", - value = "(Use the file picker UI to attach a .md file)" + encoding = @io.swagger.v3.oas.annotations.media.Encoding( + name = "file", + contentType = "application/octet-stream" ) ) ) @@ -151,8 +154,11 @@ public interface DocsSourceController { @ApiResponse(responseCode = "415", description = "Unsupported Media Type - invalid file format") }) ResponseEntity> uploadFile( - @Parameter(description = "File to upload (PDF, Markdown, or plain text)", required = true) - @RequestParam("file") @NotNull MultipartFile file + @Parameter( + description = "File to upload (PDF, Markdown, or plain text)", + required = true + ) + @RequestParam("file") MultipartFile file ); /** diff --git a/core/src/main/java/com/opencontext/service/FileStorageService.java b/core/src/main/java/com/opencontext/service/FileStorageService.java index 4d17bf2..a46c985 100644 --- a/core/src/main/java/com/opencontext/service/FileStorageService.java +++ b/core/src/main/java/com/opencontext/service/FileStorageService.java @@ -53,8 +53,8 @@ public class FileStorageService { @Value("${app.elasticsearch.index:document-chunks}") private String indexName; - // Supported file types for document processing - private static final List SUPPORTED_CONTENT_TYPES = List.of( + // Supported file types for document processing (canonical) + private static final List ALLOWED_CANONICAL_CONTENT_TYPES = List.of( "application/pdf", "text/markdown", "text/plain" @@ -69,7 +69,7 @@ public class FileStorageService { public SourceDocument uploadFileWithMetadata(MultipartFile file) { String filename = file.getOriginalFilename(); long fileSize = file.getSize(); - String contentType = file.getContentType(); + String contentType = resolveContentType(file); log.info("πŸ“€ [UPLOAD] Starting file upload with metadata: filename={}, size={} bytes, contentType={}", filename, fileSize, contentType); @@ -98,7 +98,7 @@ public SourceDocument uploadFileWithMetadata(MultipartFile file) { try { // Upload file to MinIO log.debug("☁️ [UPLOAD] Step 4/5: Uploading file to MinIO: {}", filename); - String objectKey = uploadFile(file); + String objectKey = uploadFile(file, contentType); log.info("βœ… [UPLOAD] File uploaded to MinIO successfully: {} -> {}", filename, objectKey); // Create SourceDocument entity @@ -106,7 +106,7 @@ public SourceDocument uploadFileWithMetadata(MultipartFile file) { SourceDocument sourceDocument = SourceDocument.builder() .originalFilename(file.getOriginalFilename()) .fileStoragePath(objectKey) - .fileType(determineFileType(file.getContentType())) + .fileType(determineFileType(contentType)) .fileSize(file.getSize()) .fileChecksum(fileChecksum) .ingestionStatus(IngestionStatus.PENDING) @@ -139,7 +139,7 @@ public SourceDocument uploadFileWithMetadata(MultipartFile file) { * @param file the multipart file to upload * @return the object key where the file was stored */ - public String uploadFile(MultipartFile file) { + public String uploadFile(MultipartFile file, String resolvedContentType) { try { // Ensure bucket exists ensureBucketExists(); @@ -152,7 +152,7 @@ public String uploadFile(MultipartFile file) { .bucket(minioConfig.getBucketName()) .object(objectKey) .stream(file.getInputStream(), file.getSize(), -1) - .contentType(file.getContentType()) + .contentType(resolvedContentType) .build(); minioClient.putObject(putObjectArgs); @@ -562,18 +562,23 @@ private void validateFile(MultipartFile file) { "File size exceeds maximum limit of 100MB"); } - String contentType = file.getContentType(); - if (contentType == null || !SUPPORTED_CONTENT_TYPES.contains(contentType)) { - throw new BusinessException(ErrorCode.UNSUPPORTED_MEDIA_TYPE, - "Unsupported file type. Supported types: PDF, Markdown, and plain text files."); - } - String filename = file.getOriginalFilename(); if (filename == null || filename.trim().isEmpty()) { throw new BusinessException(ErrorCode.INVALID_REQUEST, "Filename cannot be empty"); } - log.debug("βœ… [UPLOAD] File validation passed: filename={}", filename); + // Resolve canonical content type from both content-type header and filename extension + String resolvedContentType = resolveContentType(file); + + // Check if the resolved content type is supported + if (!ALLOWED_CANONICAL_CONTENT_TYPES.contains(resolvedContentType)) { + log.debug("❌ [UPLOAD] Unsupported content type: filename={}, original={}, resolved={}", + filename, file.getContentType(), resolvedContentType); + throw new BusinessException(ErrorCode.UNSUPPORTED_MEDIA_TYPE, + "Unsupported file type. Supported types: PDF, Markdown, and plain text files."); + } + + log.debug("βœ… [UPLOAD] File validation passed: filename={}, resolved_type={}", filename, resolvedContentType); } /** @@ -612,6 +617,46 @@ private String determineFileType(String contentType) { }; } + /** + * Resolves the effective content type for the uploaded file. + * If the inbound content type is null or generic (e.g., application/octet-stream), + * infer from the filename extension and normalize to a canonical, allowed type. + */ + private String resolveContentType(MultipartFile file) { + String inbound = file.getContentType(); + // If clearly an allowed canonical type, return as-is + if (ALLOWED_CANONICAL_CONTENT_TYPES.contains(inbound)) { + return inbound; + } + + // Try to infer from file extension for generic or alternate types + String filename = file.getOriginalFilename(); + String ext = (filename == null) ? null : getFileExtension(filename).toLowerCase(); + + if (ext != null) { + return switch (ext) { + case "pdf" -> "application/pdf"; + case "md", "markdown" -> "text/markdown"; + case "txt" -> "text/plain"; + default -> (inbound != null ? inbound : "application/octet-stream"); + }; + } + + // Fallback to inbound or generic + return inbound != null ? inbound : "application/octet-stream"; + } + + /** + * Returns the extension without the leading dot. Example: "README.md" -> "md". + */ + private String getFileExtension(String filename) { + int lastDot = filename.lastIndexOf('.'); + if (lastDot == -1 || lastDot == filename.length() - 1) { + return ""; + } + return filename.substring(lastDot + 1); + } + /** * Converts SourceDocument entity to DTO. */ diff --git a/core/src/main/java/com/opencontext/service/SearchService.java b/core/src/main/java/com/opencontext/service/SearchService.java index 5764f0c..d6d8f11 100644 --- a/core/src/main/java/com/opencontext/service/SearchService.java +++ b/core/src/main/java/com/opencontext/service/SearchService.java @@ -63,7 +63,7 @@ public List search(String query, int topK) { Map searchResponse = executeElasticsearchQuery(query, queryEmbedding, topK); // 3단계: 검색 κ²°κ³Όλ₯Ό DTO둜 λ³€ν™˜ - List results = parseSearchResults(searchResponse); + List results = parseSearchResults(searchResponse); long duration = System.currentTimeMillis() - startTime; log.info("ν•˜μ΄λΈŒλ¦¬λ“œ 검색 μ™„λ£Œ: query='{}', 결과수={}, μ†Œμš”μ‹œκ°„={}ms",