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 AuditService auditService;
|
||||||
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||||
private final AuditLogQueryService auditLogQueryService;
|
private final AuditLogQueryService auditLogQueryService;
|
||||||
|
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||||
|
|
||||||
public record StoreResult(Document document, boolean isNew) {}
|
public record StoreResult(Document document, boolean isNew) {}
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ public class DocumentService {
|
|||||||
if (wasPlaceholder) {
|
if (wasPlaceholder) {
|
||||||
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
||||||
}
|
}
|
||||||
|
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||||
return new StoreResult(saved, isNew);
|
return new StoreResult(saved, isNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +189,8 @@ public class DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Datei
|
// Datei
|
||||||
if (file != null && !file.isEmpty()) {
|
boolean fileUploaded = file != null && !file.isEmpty();
|
||||||
|
if (fileUploaded) {
|
||||||
FileService.UploadResult upload = fileService.uploadFile(file, file.getOriginalFilename());
|
FileService.UploadResult upload = fileService.uploadFile(file, file.getOriginalFilename());
|
||||||
doc.setFilePath(upload.s3Key());
|
doc.setFilePath(upload.s3Key());
|
||||||
doc.setFileHash(upload.fileHash());
|
doc.setFileHash(upload.fileHash());
|
||||||
@@ -197,6 +200,9 @@ public class DocumentService {
|
|||||||
|
|
||||||
Document finalDoc = documentRepository.save(doc);
|
Document finalDoc = documentRepository.save(doc);
|
||||||
documentVersionService.recordVersion(finalDoc);
|
documentVersionService.recordVersion(finalDoc);
|
||||||
|
if (fileUploaded) {
|
||||||
|
thumbnailAsyncRunner.dispatchAfterCommit(finalDoc.getId());
|
||||||
|
}
|
||||||
return finalDoc;
|
return finalDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +255,8 @@ public class DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Datei austauschen (nur wenn eine neue ausgewählt wurde)
|
// 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());
|
FileService.UploadResult upload = fileService.uploadFile(newFile, newFile.getOriginalFilename());
|
||||||
doc.setFilePath(upload.s3Key());
|
doc.setFilePath(upload.s3Key());
|
||||||
doc.setFileHash(upload.fileHash());
|
doc.setFileHash(upload.fileHash());
|
||||||
@@ -268,6 +275,10 @@ public class DocumentService {
|
|||||||
auditService.logAfterCommit(AuditKind.METADATA_UPDATED, actorId, saved.getId(), null);
|
auditService.logAfterCommit(AuditKind.METADATA_UPDATED, actorId, saved.getId(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileReplaced) {
|
||||||
|
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||||
|
}
|
||||||
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +340,7 @@ public class DocumentService {
|
|||||||
}
|
}
|
||||||
Document saved = documentRepository.save(doc);
|
Document saved = documentRepository.save(doc);
|
||||||
documentVersionService.recordVersion(saved);
|
documentVersionService.recordVersion(saved);
|
||||||
|
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
|
||||||
if (wasPlaceholder) {
|
if (wasPlaceholder) {
|
||||||
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
auditService.logAfterCommit(AuditKind.FILE_UPLOADED, actorId, saved.getId(), null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class DocumentServiceTest {
|
|||||||
@Mock AuditService auditService;
|
@Mock AuditService auditService;
|
||||||
@Mock AuditLogQueryService auditLogQueryService;
|
@Mock AuditLogQueryService auditLogQueryService;
|
||||||
@Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
|
@Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||||
|
@Mock ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||||
@InjectMocks DocumentService documentService;
|
@InjectMocks DocumentService documentService;
|
||||||
|
|
||||||
// ─── deleteDocument ───────────────────────────────────────────────────────
|
// ─── deleteDocument ───────────────────────────────────────────────────────
|
||||||
@@ -257,6 +258,107 @@ class DocumentServiceTest {
|
|||||||
verify(documentVersionService).recordVersion(any(Document.class));
|
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 ───────────────────────────────────────────────────────
|
// ─── storeDocument ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user