Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.fengting.aigcforensics.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.upload")
public record UploadPolicyProperties(
Long maxBytes,
Integer maxWidth,
Integer maxHeight,
Long maxPixels,
Integer maxFilenameLength) {

private static final long DEFAULT_MAX_BYTES = 10L * 1024 * 1024;
private static final int DEFAULT_MAX_DIMENSION = 8192;
private static final long DEFAULT_MAX_PIXELS = 25_000_000L;
private static final int DEFAULT_MAX_FILENAME_LENGTH = 255;

public UploadPolicyProperties {
maxBytes = defaultIfNull(maxBytes, DEFAULT_MAX_BYTES);
maxWidth = defaultIfNull(maxWidth, DEFAULT_MAX_DIMENSION);
maxHeight = defaultIfNull(maxHeight, DEFAULT_MAX_DIMENSION);
maxPixels = defaultIfNull(maxPixels, DEFAULT_MAX_PIXELS);
maxFilenameLength = defaultIfNull(maxFilenameLength, DEFAULT_MAX_FILENAME_LENGTH);

requirePositive("maxBytes", maxBytes);
requirePositive("maxWidth", maxWidth);
requirePositive("maxHeight", maxHeight);
requirePositive("maxPixels", maxPixels);
requirePositive("maxFilenameLength", maxFilenameLength);
}

private static long defaultIfNull(Long value, long defaultValue) {
return value == null ? defaultValue : value;
}

private static int defaultIfNull(Integer value, int defaultValue) {
return value == null ? defaultValue : value;
}

private static void requirePositive(String name, long value) {
if (value <= 0) {
throw new IllegalArgumentException(name + " must be positive");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

import com.fengting.aigcforensics.dto.error.ErrorResponse;
Expand Down Expand Up @@ -33,6 +34,12 @@ public ErrorResponse handleMissingPart(MissingServletRequestPartException except
return new ErrorResponse("Missing required multipart field: " + exception.getRequestPartName(), Instant.now());
}

@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
public ErrorResponse handleUploadTooLarge(MaxUploadSizeExceededException exception) {
return new ErrorResponse("Uploaded file exceeds the configured request limit", Instant.now());
}

@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -26,23 +24,19 @@
import com.fengting.aigcforensics.repository.DetectionTaskRepository;
import com.fengting.aigcforensics.repository.MediaAssetRepository;
import com.fengting.aigcforensics.repository.ModelPredictionRepository;
import com.fengting.aigcforensics.service.ImageMetadataService.ImageMetadata;
import com.fengting.aigcforensics.config.UploadPolicyProperties;
import com.fengting.aigcforensics.service.StorageService.StoredFile;

@Service
public class DetectionWorkflowService {

private static final Set<String> SUPPORTED_IMAGE_TYPES = Set.of(
"image/jpeg",
"image/png",
"image/webp");
private static final String UNSUPPORTED_IMAGE_MESSAGE = "Only JPEG, PNG, and WebP images are supported";

private final MediaAssetRepository mediaAssetRepository;
private final DetectionTaskRepository detectionTaskRepository;
private final StorageService storageService;
private final HashService hashService;
private final ImageMetadataService imageMetadataService;
private final ImageUploadInspector imageUploadInspector;
private final UploadFilenameSanitizer filenameSanitizer;
private final UploadPolicyProperties uploadPolicy;
private final ModelPredictionRepository modelPredictionRepository;
private final DetectionReportRepository detectionReportRepository;
private final Clock clock;
Expand All @@ -53,15 +47,19 @@ public DetectionWorkflowService(
DetectionTaskRepository detectionTaskRepository,
StorageService storageService,
HashService hashService,
ImageMetadataService imageMetadataService,
ImageUploadInspector imageUploadInspector,
UploadFilenameSanitizer filenameSanitizer,
UploadPolicyProperties uploadPolicy,
ModelPredictionRepository modelPredictionRepository,
DetectionReportRepository detectionReportRepository) {
this(
mediaAssetRepository,
detectionTaskRepository,
storageService,
hashService,
imageMetadataService,
imageUploadInspector,
filenameSanitizer,
uploadPolicy,
modelPredictionRepository,
detectionReportRepository,
Clock.systemUTC());
Expand All @@ -72,15 +70,19 @@ public DetectionWorkflowService(
DetectionTaskRepository detectionTaskRepository,
StorageService storageService,
HashService hashService,
ImageMetadataService imageMetadataService,
ImageUploadInspector imageUploadInspector,
UploadFilenameSanitizer filenameSanitizer,
UploadPolicyProperties uploadPolicy,
ModelPredictionRepository modelPredictionRepository,
DetectionReportRepository detectionReportRepository,
Clock clock) {
this.mediaAssetRepository = mediaAssetRepository;
this.detectionTaskRepository = detectionTaskRepository;
this.storageService = storageService;
this.hashService = hashService;
this.imageMetadataService = imageMetadataService;
this.imageUploadInspector = imageUploadInspector;
this.filenameSanitizer = filenameSanitizer;
this.uploadPolicy = uploadPolicy;
this.modelPredictionRepository = modelPredictionRepository;
this.detectionReportRepository = detectionReportRepository;
this.clock = clock;
Expand All @@ -91,9 +93,10 @@ public CreateImageDetectionResponse createImageDetection(MultipartFile file) {
validateUpload(file);

byte[] content = readContent(file);
InspectedImage inspectedImage = imageUploadInspector.inspect(content);
String sha256 = hashService.sha256(content);
MediaAsset asset = mediaAssetRepository.findBySha256(sha256)
.orElseGet(() -> storeNewAsset(file, content, sha256));
.orElseGet(() -> storeNewAsset(file, content, sha256, inspectedImage));

DetectionTask task = new DetectionTask(
newExternalId("task"),
Expand Down Expand Up @@ -216,26 +219,30 @@ private void validateUpload(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Uploaded file must not be empty");
}

String contentType = normalizeContentType(file.getContentType());
if (!SUPPORTED_IMAGE_TYPES.contains(contentType)) {
throw new IllegalArgumentException(UNSUPPORTED_IMAGE_MESSAGE);
if (file.getSize() > uploadPolicy.maxBytes()) {
throw new IllegalArgumentException("Uploaded file exceeds the configured byte limit");
}
}

private MediaAsset storeNewAsset(MultipartFile file, byte[] content, String sha256) {
private MediaAsset storeNewAsset(
MultipartFile file,
byte[] content,
String sha256,
InspectedImage inspectedImage) {
String assetId = newExternalId("asset");
StoredFile storedFile = storageService.saveUpload(assetId, file.getOriginalFilename(), content);
ImageMetadata metadata = imageMetadataService.read(storedFile.path());
StoredFile storedFile = storageService.saveAcceptedImage(
assetId,
inspectedImage.extension(),
content);

MediaAsset asset = new MediaAsset(
assetId,
safeOriginalFilename(file),
normalizeContentType(file.getContentType()),
filenameSanitizer.sanitize(file.getOriginalFilename()),
inspectedImage.contentType(),
storedFile.size(),
sha256,
metadata.width(),
metadata.height(),
inspectedImage.width(),
inspectedImage.height(),
storedFile.path().toString(),
null,
Instant.now(clock));
Expand All @@ -250,21 +257,6 @@ private byte[] readContent(MultipartFile file) {
}
}

private String safeOriginalFilename(MultipartFile file) {
String filename = file.getOriginalFilename();
if (filename == null || filename.isBlank()) {
return "upload.bin";
}
return filename;
}

private String normalizeContentType(String contentType) {
if (contentType == null) {
return "";
}
return contentType.toLowerCase(Locale.ROOT);
}

private String newExternalId(String prefix) {
return prefix + "_" + UUID.randomUUID().toString().replace("-", "");
}
Expand Down

This file was deleted.

Loading
Loading