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 springdocUi = "3.0.3"
|
||||||
val awsSdk = "2.26.0"
|
val awsSdk = "2.26.0"
|
||||||
val thumbnailator = "0.4.20"
|
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-actuator")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
@@ -51,6 +52,7 @@ dependencies {
|
|||||||
implementation(platform("software.amazon.awssdk:bom:$awsSdk"))
|
implementation(platform("software.amazon.awssdk:bom:$awsSdk"))
|
||||||
implementation("software.amazon.awssdk:s3")
|
implementation("software.amazon.awssdk:s3")
|
||||||
implementation("net.coobird:thumbnailator:$thumbnailator")
|
implementation("net.coobird:thumbnailator:$thumbnailator")
|
||||||
|
implementation("org.apache.tika:tika-core:$tika")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.halfbinary.scavengerhuntapi.error
|
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.InvalidEmailException
|
||||||
import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException
|
import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException
|
||||||
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
|
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.ExceptionHandler
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
|
|
||||||
@RestControllerAdvice
|
@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?> {
|
private fun simpleMap(key: String, value: String?): Map<String, String?> {
|
||||||
return mapOf(Pair(key, value))
|
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
|
package net.halfbinary.scavengerhuntapi.service
|
||||||
|
|
||||||
import net.coobird.thumbnailator.Thumbnails
|
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.HuntId
|
||||||
import net.halfbinary.scavengerhuntapi.model.ItemId
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||||
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
||||||
@@ -18,9 +20,21 @@ import java.time.LocalDateTime
|
|||||||
class PhotoService(
|
class PhotoService(
|
||||||
private val photoRepository: PhotoRepository,
|
private val photoRepository: PhotoRepository,
|
||||||
private val hunterService: HunterService,
|
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 {
|
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 hunter = hunterService.getHunterByEmail(email)
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val photo = Photo(
|
val photo = Photo(
|
||||||
@@ -34,14 +48,25 @@ class PhotoService(
|
|||||||
val savedRecord = photoRepository.save(photo.toRecord())
|
val savedRecord = photoRepository.save(photo.toRecord())
|
||||||
val baseName = savedRecord.id.toString()
|
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${fileProbeService.getFileExtension(fileType)}", originalBytes, fileType)
|
||||||
s3StorageService.upload("${baseName}_thumb.jpg", resize(originalBytes, 200), MediaType.IMAGE_JPEG_VALUE)
|
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
|
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 {
|
private fun resize(bytes: ByteArray, width: Int): ByteArray {
|
||||||
val output = ByteArrayOutputStream()
|
val output = ByteArrayOutputStream()
|
||||||
Thumbnails.of(ByteArrayInputStream(bytes))
|
Thumbnails.of(ByteArrayInputStream(bytes))
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ minio.access-key=${MINIO_ACCESS_KEY}
|
|||||||
minio.secret-key=${MINIO_SECRET_KEY}
|
minio.secret-key=${MINIO_SECRET_KEY}
|
||||||
minio.bucket=${MINIO_BUCKET}
|
minio.bucket=${MINIO_BUCKET}
|
||||||
|
|
||||||
spring.servlet.multipart.max-file-size=10MB
|
spring.servlet.multipart.max-file-size=25MB
|
||||||
spring.servlet.multipart.max-request-size=10MB
|
spring.servlet.multipart.max-request-size=25MB
|
||||||
|
|
||||||
springdoc.api-docs.enabled=true
|
springdoc.api-docs.enabled=true
|
||||||
springdoc.api-docs.path=/docs/api-docs
|
springdoc.api-docs.path=/docs/api-docs
|
||||||
|
|||||||
Reference in New Issue
Block a user