diff --git a/build.gradle.kts b/build.gradle.kts index 079485c..1cd21b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { val springdocUi = "3.0.3" val awsSdk = "2.26.0" val thumbnailator = "0.4.20" + val tika = "3.3.0" implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") @@ -51,6 +52,7 @@ dependencies { implementation(platform("software.amazon.awssdk:bom:$awsSdk")) implementation("software.amazon.awssdk:s3") implementation("net.coobird:thumbnailator:$thumbnailator") + implementation("org.apache.tika:tika-core:$tika") developmentOnly("org.springframework.boot:spring-boot-devtools") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") testImplementation("org.springframework.boot:spring-boot-starter-actuator-test") diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt index a4c3397..fcbe419 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt @@ -1,5 +1,6 @@ package net.halfbinary.scavengerhuntapi.error +import net.halfbinary.scavengerhuntapi.error.exception.BadFileException import net.halfbinary.scavengerhuntapi.error.exception.InvalidEmailException import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException @@ -12,6 +13,8 @@ import org.springframework.web.bind.MethodArgumentNotValidException 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 java.net.SocketTimeoutException @RestControllerAdvice @@ -68,6 +71,24 @@ class ExceptionHandler { } } + @ExceptionHandler(BadFileException::class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + fun badFileException(e: BadFileException): String? { + return e.message + } + + @ExceptionHandler(MaxUploadSizeExceededException::class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + fun maxUploadSizeExceededException(e: MaxUploadSizeExceededException): String? { + return e.message + } + + @ExceptionHandler(SocketTimeoutException::class) + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + fun socketTimeoutException(): String { + return "Unable to connect. Try again later." + } + private fun simpleMap(key: String, value: String?): Map { return mapOf(Pair(key, value)) } diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/BadFileException.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/BadFileException.kt new file mode 100644 index 0000000..8d7cc37 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/BadFileException.kt @@ -0,0 +1,3 @@ +package net.halfbinary.scavengerhuntapi.error.exception + +class BadFileException(override val message: String): RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/FileProbeService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/FileProbeService.kt new file mode 100644 index 0000000..480a51e --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/FileProbeService.kt @@ -0,0 +1,22 @@ +package net.halfbinary.scavengerhuntapi.service + +import org.apache.tika.Tika +import org.apache.tika.config.TikaConfig +import org.springframework.stereotype.Service + + +@Service +class FileProbeService { + private val tika = Tika() + + fun getFileType(fileBytes: ByteArray): String { + return tika.detect(fileBytes) + } + + fun getFileExtension(fileType: String): String { + return TikaConfig.getDefaultConfig().mimeRepository.forName(fileType).extension + } + fun isImageType(fileType: String): Boolean { + return fileType.startsWith("image") + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt index b09d159..e7a2d09 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt @@ -1,6 +1,8 @@ package net.halfbinary.scavengerhuntapi.service import net.coobird.thumbnailator.Thumbnails +import net.coobird.thumbnailator.tasks.UnsupportedFormatException +import net.halfbinary.scavengerhuntapi.error.exception.BadFileException import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.ItemId import net.halfbinary.scavengerhuntapi.model.PhotoStatus @@ -18,9 +20,21 @@ import java.time.LocalDateTime class PhotoService( private val photoRepository: PhotoRepository, private val hunterService: HunterService, - private val s3StorageService: S3StorageService + private val s3StorageService: S3StorageService, + private val fileProbeService: FileProbeService ) { fun submitPhoto(huntId: HuntId, itemId: ItemId, email: String, file: MultipartFile): Photo { + val originalBytes = file.bytes + val fileType = fileProbeService.getFileType(originalBytes) + + if(!fileProbeService.isImageType(fileType)) throw BadFileException("Not an image") + + val originalAsJpeg = try { + toJpeg(file.bytes) + } catch (_: UnsupportedFormatException) { + throw BadFileException("Image type is not supported") + } + val hunter = hunterService.getHunterByEmail(email) val now = LocalDateTime.now() val photo = Photo( @@ -34,14 +48,25 @@ class PhotoService( val savedRecord = photoRepository.save(photo.toRecord()) val baseName = savedRecord.id.toString() - val originalBytes = file.bytes - s3StorageService.upload("$baseName.jpg", originalBytes, MediaType.IMAGE_JPEG_VALUE) - s3StorageService.upload("${baseName}_medium.jpg", resize(originalBytes, 800), MediaType.IMAGE_JPEG_VALUE) - s3StorageService.upload("${baseName}_thumb.jpg", resize(originalBytes, 200), MediaType.IMAGE_JPEG_VALUE) + + + s3StorageService.upload("$baseName${fileProbeService.getFileExtension(fileType)}", originalBytes, fileType) + s3StorageService.upload("${baseName}_large.jpg", originalAsJpeg, MediaType.IMAGE_JPEG_VALUE) + s3StorageService.upload("${baseName}_medium.jpg", resize(originalBytes, 800), MediaType.IMAGE_JPEG_VALUE) + s3StorageService.upload("${baseName}_small.jpg", resize(originalBytes, 200), MediaType.IMAGE_JPEG_VALUE) return photo } + private fun toJpeg(bytes: ByteArray): ByteArray { + val output = ByteArrayOutputStream() + Thumbnails.of(ByteArrayInputStream(bytes)) + .scale(1.0) + .outputFormat("jpg") + .toOutputStream(output) + return output.toByteArray() + } + private fun resize(bytes: ByteArray, width: Int): ByteArray { val output = ByteArrayOutputStream() Thumbnails.of(ByteArrayInputStream(bytes)) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4bee810..d8bc103 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,8 +17,8 @@ minio.access-key=${MINIO_ACCESS_KEY} minio.secret-key=${MINIO_SECRET_KEY} minio.bucket=${MINIO_BUCKET} -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB +spring.servlet.multipart.max-file-size=25MB +spring.servlet.multipart.max-request-size=25MB springdoc.api-docs.enabled=true springdoc.api-docs.path=/docs/api-docs