fix: store content type at upload time instead of guessing from extension

Previously FileService fell back to extension-based MIME detection, causing
TIFF, HEIC, DOCX and other unlisted types to be served as octet-stream
(forced download instead of inline display).

- Add content_type column to documents (V3 migration)
- Store file.getContentType() in DocumentService on upload and file replace
- MassImportService uses Files.probeContentType() for local files
- DocumentController prefers doc.getContentType() over S3-reported type
- FileService: remove extension-based fallback (no longer needed)
- DocumentService: replace leftover ResponseStatusException with DomainException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-15 14:42:19 +01:00
parent 1819829d6e
commit 79eccd5598
6 changed files with 30 additions and 18 deletions

View File

@@ -57,8 +57,13 @@ public class DocumentController {
try {
FileService.S3FileDownload download = fileService.downloadFile(doc.getFilePath());
// Prefer the content type stored at upload time; fall back to whatever S3 reports
String contentType = (doc.getContentType() != null && !doc.getContentType().isBlank())
? doc.getContentType()
: download.contentType();
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(download.contentType()))
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + doc.getOriginalFilename() + "\"")
.body(download.resource());
} catch (FileService.StorageFileNotFoundException e) {

View File

@@ -32,6 +32,10 @@ public class Document {
@Column(name = "file_path")
private String filePath;
// MIME-Type, gespeichert beim Upload (z.B. "application/pdf", "image/jpeg")
@Column(name = "content_type")
private String contentType;
// Originaler Dateiname beim Upload (z.B. "Brief_Oma_1940.pdf")
@Column(name = "original_filename", nullable = false)
private String originalFilename;

View File

@@ -13,11 +13,11 @@ import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.time.LocalDate;
@@ -69,6 +69,7 @@ public class DocumentService {
// 3. Update Database
document.setFilePath(s3Key);
document.setContentType(file.getContentType());
if (document.getStatus() == DocumentStatus.PLACEHOLDER) {
document.setStatus(DocumentStatus.UPLOADED);
}
@@ -79,7 +80,7 @@ public class DocumentService {
@Transactional
public Document updateDocument(UUID id, DocumentUpdateDTO dto, MultipartFile newFile) throws IOException {
Document doc = documentRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Dokument nicht gefunden"));
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
// 1. Einfache Felder Update
doc.setTitle(dto.getTitle());
@@ -125,6 +126,7 @@ public class DocumentService {
doc.setFilePath(s3Key);
doc.setOriginalFilename(newFile.getOriginalFilename());
doc.setContentType(newFile.getContentType());
doc.setStatus(DocumentStatus.UPLOADED);
}

View File

@@ -66,21 +66,10 @@ public class FileService {
ResponseInputStream<GetObjectResponse> s3Object = s3Client.getObject(getObjectRequest);
// 1. Versuche Content-Type von S3 zu bekommen
// Use whatever content type S3 has stored (set at upload time)
String contentType = s3Object.response().contentType();
// 2. FIX: Wenn S3 "octet-stream" sagt (oder null ist), raten wir anhand der Endung
if (contentType == null || contentType.isEmpty() || contentType.equals("application/octet-stream")) {
String keyLower = s3Key.toLowerCase();
if (keyLower.endsWith(".pdf")) {
contentType = "application/pdf";
} else if (keyLower.endsWith(".jpg") || keyLower.endsWith(".jpeg")) {
contentType = "image/jpeg";
} else if (keyLower.endsWith(".png")) {
contentType = "image/png";
} else {
contentType = "application/octet-stream"; // Fallback
}
if (contentType == null || contentType.isBlank()) {
contentType = "application/octet-stream";
}
return new S3FileDownload(new InputStreamResource(s3Object), contentType);

View File

@@ -140,12 +140,22 @@ public class MassImportService {
return;
}
// Detect MIME type from the local file
String contentType;
try {
contentType = Files.probeContentType(file.toPath());
} catch (IOException e) {
contentType = null;
}
if (contentType == null) contentType = "application/octet-stream";
// Upload zu S3
String s3Key = "documents/" + UUID.randomUUID() + "_" + file.getName();
try {
s3Client.putObject(PutObjectRequest.builder()
.bucket(bucketName)
.key(s3Key)
.contentType(contentType)
.build(),
RequestBody.fromFile(file));
} catch (Exception e) {
@@ -160,6 +170,7 @@ public class MassImportService {
.build());
doc.setFilePath(s3Key);
doc.setContentType(contentType);
doc.setStatus(DocumentStatus.UPLOADED); // Jetzt ist es da!
doc.setDocumentDate(date);
doc.setLocation(location);

View File

@@ -0,0 +1 @@
ALTER TABLE documents ADD COLUMN content_type character varying(255);