Adds file type detection, and beefs up error states for file uploads
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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<String, String?> {
|
||||
return mapOf(Pair(key, value))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.halfbinary.scavengerhuntapi.error.exception
|
||||
|
||||
class BadFileException(override val message: String): RuntimeException(message)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user