feat(backend): dispatch thumbnail generation from DocumentService upload paths
All four upload code paths (storeDocument, createDocument, updateDocument, attachFile) now call thumbnailAsyncRunner.dispatchAfterCommit(id) after the document save. createDocument and updateDocument only dispatch when a file was actually provided/replaced. The dispatch is afterCommit-safe: if the surrounding @Transactional method rolls back, no thumbnail is generated for a document that never reached the DB. Refs #307 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ public class DocumentService {
|
||||
private final AuditService auditService;
|
||||
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||
private final AuditLogQueryService auditLogQueryService;
|
||||
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||
|
||||
public record StoreResult(Document document, boolean isNew) {}
|
||||
|
||||
@@ -125,6 +126,7 @@ public class DocumentService {
|
||||
if (wasPlaceholder) {
|
||||
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
||||
}
|
||||
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||
return new StoreResult(saved, isNew);
|
||||
}
|
||||
|
||||
@@ -187,7 +189,8 @@ public class DocumentService {
|
||||
}
|
||||
|
||||
// Datei
|
||||
if (file != null && !file.isEmpty()) {
|
||||
boolean fileUploaded = file != null && !file.isEmpty();
|
||||
if (fileUploaded) {
|
||||
FileService.UploadResult upload = fileService.uploadFile(file, file.getOriginalFilename());
|
||||
doc.setFilePath(upload.s3Key());
|
||||
doc.setFileHash(upload.fileHash());
|
||||
@@ -197,6 +200,9 @@ public class DocumentService {
|
||||
|
||||
Document finalDoc = documentRepository.save(doc);
|
||||
documentVersionService.recordVersion(finalDoc);
|
||||
if (fileUploaded) {
|
||||
thumbnailAsyncRunner.dispatchAfterCommit(finalDoc.getId());
|
||||
}
|
||||
return finalDoc;
|
||||
}
|
||||
|
||||
@@ -249,7 +255,8 @@ public class DocumentService {
|
||||
}
|
||||
|
||||
// 4. Datei austauschen (nur wenn eine neue ausgewählt wurde)
|
||||
if (newFile != null && !newFile.isEmpty()) {
|
||||
boolean fileReplaced = newFile != null && !newFile.isEmpty();
|
||||
if (fileReplaced) {
|
||||
FileService.UploadResult upload = fileService.uploadFile(newFile, newFile.getOriginalFilename());
|
||||
doc.setFilePath(upload.s3Key());
|
||||
doc.setFileHash(upload.fileHash());
|
||||
@@ -268,6 +275,10 @@ public class DocumentService {
|
||||
auditService.logAfterCommit(AuditKind.METADATA_UPDATED, actorId, saved.getId(), null);
|
||||
}
|
||||
|
||||
if (fileReplaced) {
|
||||
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -329,6 +340,7 @@ public class DocumentService {
|
||||
}
|
||||
Document saved = documentRepository.save(doc);
|
||||
documentVersionService.recordVersion(saved);
|
||||
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||
if (wasPlaceholder) {
|
||||
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class DocumentServiceTest {
|
||||
@Mock AuditService auditService;
|
||||
@Mock AuditLogQueryService auditLogQueryService;
|
||||
@Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||
@Mock ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||
@InjectMocks DocumentService documentService;
|
||||
|
||||
// ─── deleteDocument ───────────────────────────────────────────────────────
|
||||
@@ -257,6 +258,107 @@ class DocumentServiceTest {
|
||||
verify(documentVersionService).recordVersion(any(Document.class));
|
||||
}
|
||||
|
||||
// ─── thumbnail dispatch ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void storeDocument_dispatchesThumbnailAfterSave() throws Exception {
|
||||
org.springframework.mock.web.MockMultipartFile file =
|
||||
new org.springframework.mock.web.MockMultipartFile("file", "new.pdf", "application/pdf", new byte[]{1});
|
||||
UUID savedId = UUID.randomUUID();
|
||||
Document saved = Document.builder().id(savedId).originalFilename("new.pdf").build();
|
||||
when(documentRepository.findFirstByOriginalFilename("new.pdf")).thenReturn(Optional.empty());
|
||||
when(documentRepository.save(any())).thenReturn(saved);
|
||||
when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("documents/new.pdf", "hash"));
|
||||
|
||||
documentService.storeDocument(file, null);
|
||||
|
||||
verify(thumbnailAsyncRunner, times(1)).dispatchAfterCommit(savedId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDocument_dispatchesThumbnail_onlyWhenFileProvided() throws Exception {
|
||||
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||
dto.setTitle("No file");
|
||||
UUID savedId = UUID.randomUUID();
|
||||
Document saved = Document.builder().id(savedId).title("No file")
|
||||
.originalFilename("No file").status(DocumentStatus.PLACEHOLDER).build();
|
||||
when(documentRepository.save(any())).thenReturn(saved);
|
||||
when(documentRepository.findById(any())).thenReturn(Optional.of(saved));
|
||||
|
||||
documentService.createDocument(dto, null);
|
||||
|
||||
verifyNoInteractions(thumbnailAsyncRunner);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDocument_dispatchesThumbnail_whenFileProvided() throws Exception {
|
||||
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||
dto.setTitle("With file");
|
||||
org.springframework.mock.web.MockMultipartFile file =
|
||||
new org.springframework.mock.web.MockMultipartFile("file", "scan.pdf", "application/pdf", new byte[]{1});
|
||||
UUID savedId = UUID.randomUUID();
|
||||
Document saved = Document.builder().id(savedId).title("With file")
|
||||
.originalFilename("scan.pdf").status(DocumentStatus.PLACEHOLDER).build();
|
||||
when(documentRepository.save(any())).thenReturn(saved);
|
||||
when(documentRepository.findById(any())).thenReturn(Optional.of(saved));
|
||||
when(fileService.uploadFile(any(), any()))
|
||||
.thenReturn(new FileService.UploadResult("documents/scan.pdf", "hash"));
|
||||
|
||||
documentService.createDocument(dto, file);
|
||||
|
||||
verify(thumbnailAsyncRunner, times(1)).dispatchAfterCommit(savedId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateDocument_dispatchesThumbnail_onlyWhenFileReplaced() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Document existing = Document.builder()
|
||||
.id(id).title("Doc").originalFilename("old.pdf")
|
||||
.status(DocumentStatus.UPLOADED).build();
|
||||
when(documentRepository.findById(id)).thenReturn(Optional.of(existing));
|
||||
when(documentRepository.save(any())).thenReturn(existing);
|
||||
|
||||
documentService.updateDocument(id, new DocumentUpdateDTO(), null, null);
|
||||
|
||||
verifyNoInteractions(thumbnailAsyncRunner);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateDocument_dispatchesThumbnail_whenNewFileProvided() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Document existing = Document.builder()
|
||||
.id(id).title("Doc").originalFilename("old.pdf")
|
||||
.status(DocumentStatus.UPLOADED).build();
|
||||
org.springframework.mock.web.MockMultipartFile newFile =
|
||||
new org.springframework.mock.web.MockMultipartFile("file", "new.pdf", "application/pdf", new byte[]{1});
|
||||
when(documentRepository.findById(id)).thenReturn(Optional.of(existing));
|
||||
when(documentRepository.save(any())).thenReturn(existing);
|
||||
when(fileService.uploadFile(any(), any()))
|
||||
.thenReturn(new FileService.UploadResult("documents/new.pdf", "hash"));
|
||||
|
||||
documentService.updateDocument(id, new DocumentUpdateDTO(), newFile, null);
|
||||
|
||||
verify(thumbnailAsyncRunner, times(1)).dispatchAfterCommit(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void attachFile_dispatchesThumbnailAfterSave() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Document existing = Document.builder()
|
||||
.id(id).title("Placeholder").originalFilename("placeholder")
|
||||
.status(DocumentStatus.PLACEHOLDER).build();
|
||||
org.springframework.mock.web.MockMultipartFile file =
|
||||
new org.springframework.mock.web.MockMultipartFile("file", "scan.pdf", "application/pdf", new byte[]{1});
|
||||
when(documentRepository.findById(id)).thenReturn(Optional.of(existing));
|
||||
when(documentRepository.save(any())).thenReturn(existing);
|
||||
when(fileService.uploadFile(any(), any()))
|
||||
.thenReturn(new FileService.UploadResult("documents/scan.pdf", "hash"));
|
||||
|
||||
documentService.attachFile(id, file, null);
|
||||
|
||||
verify(thumbnailAsyncRunner, times(1)).dispatchAfterCommit(id);
|
||||
}
|
||||
|
||||
// ─── storeDocument ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user