feat(upload): add POST /api/documents/quick-upload endpoint for bulk file upload

Adds a new multipart endpoint that accepts multiple files and creates one
document per file without requiring any form metadata. Each document gets
title = filename-without-extension and status = UPLOADED.

- Fix storeDocument() to strip the file extension from the document title
- Validate content type (PDF/JPEG/PNG/TIFF) server-side; unsupported files
  are skipped and returned as per-file errors in QuickUploadResult
- Tests cover 401/403 auth, success path, and unsupported file type

Closes #66 (backend part)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-26 09:59:59 +01:00
parent 29a71f4421
commit 332b5b3c40
4 changed files with 126 additions and 1 deletions

View File

@@ -2,7 +2,9 @@ package org.raddatz.familienarchiv.controller;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -103,6 +105,40 @@ public class DocumentController {
}
}
// --- QUICK UPLOAD ---
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
"application/pdf", "image/jpeg", "image/png", "image/tiff");
public record QuickUploadResult(List<Document> created, List<String> errors) {}
@PostMapping(value = "/quick-upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RequirePermission(Permission.WRITE_ALL)
public QuickUploadResult quickUpload(
@RequestPart(value = "files", required = false) List<MultipartFile> files) {
List<Document> created = new ArrayList<>();
List<String> errors = new ArrayList<>();
if (files == null || files.isEmpty()) {
return new QuickUploadResult(created, errors);
}
for (MultipartFile file : files) {
if (!ALLOWED_CONTENT_TYPES.contains(file.getContentType())) {
errors.add(file.getOriginalFilename() + ": unsupported file type");
continue;
}
try {
created.add(documentService.storeDocument(file));
} catch (Exception e) {
errors.add(file.getOriginalFilename() + ": " + e.getMessage());
log.warn("Quick upload failed for file {}: {}", file.getOriginalFilename(), e.getMessage());
}
}
return new QuickUploadResult(created, errors);
}
@GetMapping("/search")
public ResponseEntity<List<Document>> search(
@RequestParam(required = false) String q,

View File

@@ -61,7 +61,7 @@ public class DocumentService {
} else {
document = Document.builder()
.originalFilename(originalFilename)
.title(originalFilename)
.title(stripExtension(originalFilename))
.status(DocumentStatus.UPLOADED)
.build();
}
@@ -307,6 +307,12 @@ public class DocumentService {
// ─── private helpers ──────────────────────────────────────────────────────
private static String stripExtension(String filename) {
if (filename == null) return null;
int dot = filename.lastIndexOf('.');
return dot > 0 ? filename.substring(0, dot) : filename;
}
private static String sha256Hex(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");