Adds image retrieval endpoint
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package net.halfbinary.scavengerhuntapi.controller
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("admin")
|
||||
class AdminController {
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Tag(name = "Admin")
|
||||
@PostMapping("/admin/photo/{photoId}")
|
||||
@Operation(summary = "Sets a review status for the specified photo")
|
||||
fun reviewPhoto(@PathVariable photoId: PhotoId) {
|
||||
TODO("Set a review status for the specified photo, and update the photo record's status change timestamp")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.halfbinary.scavengerhuntapi.controller
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import net.halfbinary.scavengerhuntapi.model.ImageVersion
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
||||
import net.halfbinary.scavengerhuntapi.service.PhotoService
|
||||
import org.springframework.core.io.InputStreamSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("photo")
|
||||
class PhotoController(private val photoService: PhotoService) {
|
||||
@GetMapping("/{photoId}/file")
|
||||
@Operation(summary = "Get the binary image information for the specified Photo")
|
||||
fun getPhoto(authentication: Authentication,
|
||||
@PathVariable photoId: PhotoId,
|
||||
@RequestParam(defaultValue = "LARGE") version: ImageVersion): ResponseEntity<InputStreamSource> {
|
||||
val photoFile = photoService.getPhotoFile(photoId, authentication.name, version)
|
||||
return ResponseEntity.ok()
|
||||
.contentType(photoFile.contentType)
|
||||
.body(photoFile.resource)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import net.halfbinary.scavengerhuntapi.model.response.TeamItemResponse
|
||||
import net.halfbinary.scavengerhuntapi.model.response.TeamResponse
|
||||
import net.halfbinary.scavengerhuntapi.service.PhotoService
|
||||
import net.halfbinary.scavengerhuntapi.service.TeamService
|
||||
import org.springframework.core.io.InputStreamSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
@@ -74,15 +73,6 @@ class TeamController(private val teamService: TeamService, private val photoServ
|
||||
return ResponseEntity.ok(photoService.getPhotoInfo(huntId, teamId, itemId, photoId, authentication.name))
|
||||
}
|
||||
|
||||
@GetMapping("/{teamId}/item/{itemId}/photo/{photoId}/file")
|
||||
@Operation(summary = "Get the binary image information for the specified Team, Hunt, Item, and Photo")
|
||||
fun getPhoto(@PathVariable huntId: HuntId,
|
||||
@PathVariable teamId: TeamId,
|
||||
@PathVariable itemId: ItemId,
|
||||
@PathVariable photoId: PhotoId): ResponseEntity<InputStreamSource> {
|
||||
TODO("Get the binary image information for the specified Team, Hunt, Item, and Photo")
|
||||
}
|
||||
|
||||
@PostMapping("/{teamId}/item/{itemId}/photo")
|
||||
@Operation(summary = "Save photo information and store the binary file")
|
||||
fun submitPhoto(@PathVariable huntId: HuntId,
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.method.annotation.MethodArgumentTypeMismatchException
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
@@ -96,6 +97,12 @@ class ExceptionHandler {
|
||||
return "Unable to connect. Try again later."
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException::class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
fun argumentMismatchException(): String? {
|
||||
return "Invalid parameter value."
|
||||
}
|
||||
|
||||
private fun simpleMap(key: String, value: String?): Map<String, String?> {
|
||||
return mapOf(Pair(key, value))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.halfbinary.scavengerhuntapi.model
|
||||
|
||||
enum class ImageVersion {
|
||||
ORIGINAL, LARGE, MEDIUM, SMALL
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.domain
|
||||
|
||||
import org.springframework.core.io.InputStreamResource
|
||||
import org.springframework.http.MediaType
|
||||
|
||||
data class PhotoFile(val resource: InputStreamResource, val contentType: MediaType)
|
||||
@@ -7,15 +7,19 @@ import net.halfbinary.scavengerhuntapi.error.exception.ForbiddenException
|
||||
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
|
||||
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||
import net.halfbinary.scavengerhuntapi.model.ImageVersion
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
||||
import net.halfbinary.scavengerhuntapi.model.TeamId
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.PhotoFile
|
||||
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
|
||||
import net.halfbinary.scavengerhuntapi.model.converter.toRecord
|
||||
import net.halfbinary.scavengerhuntapi.model.converter.toResponse
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.Photo
|
||||
import net.halfbinary.scavengerhuntapi.model.response.PhotoResponse
|
||||
import net.halfbinary.scavengerhuntapi.repository.PhotoRepository
|
||||
import org.springframework.core.io.InputStreamResource
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
@@ -56,7 +60,7 @@ class PhotoService(
|
||||
val savedRecord = photoRepository.save(photo.toRecord())
|
||||
val baseName = savedRecord.id.toString()
|
||||
|
||||
s3StorageService.upload("$baseName${fileProbeService.getFileExtension(fileType)}", originalBytes, fileType)
|
||||
s3StorageService.upload("${baseName}_original${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)
|
||||
@@ -76,6 +80,29 @@ class PhotoService(
|
||||
return photoRecord.toDomain().toResponse(submitter)
|
||||
}
|
||||
|
||||
fun getPhotoFile(photoId: PhotoId, email: String, version: ImageVersion = ImageVersion.LARGE): PhotoFile {
|
||||
val requestingHunter = hunterService.getHunterByEmail(email)
|
||||
val photoRecord = photoRepository.findByIdOrNull(photoId)
|
||||
?: throw NotFoundException("Photo not found")
|
||||
|
||||
if (!requestingHunter.isAdmin) {
|
||||
val submitter = hunterService.getHunterById(photoRecord.hunterId)
|
||||
val requestingTeam = teamService.getTeamForHunterInHunt(photoRecord.huntId, requestingHunter.email)
|
||||
val submitterTeam = teamService.getTeamForHunterInHunt(photoRecord.huntId, submitter.email)
|
||||
if (requestingTeam.id != submitterTeam.id) throw ForbiddenException("Access denied")
|
||||
}
|
||||
|
||||
val key = when (version) {
|
||||
ImageVersion.ORIGINAL -> s3StorageService.findKeyByPrefix("${photoId}_original")
|
||||
?: throw NotFoundException("Photo file not found")
|
||||
ImageVersion.LARGE -> "${photoId}_large.jpg"
|
||||
ImageVersion.MEDIUM -> "${photoId}_medium.jpg"
|
||||
ImageVersion.SMALL -> "${photoId}_small.jpg"
|
||||
}
|
||||
val (stream, contentType) = s3StorageService.download(key)
|
||||
return PhotoFile(InputStreamResource(stream), MediaType.parseMediaType(contentType))
|
||||
}
|
||||
|
||||
private fun toJpeg(bytes: ByteArray): ByteArray {
|
||||
val output = ByteArrayOutputStream()
|
||||
Thumbnails.of(ByteArrayInputStream(bytes))
|
||||
|
||||
@@ -4,7 +4,10 @@ import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import software.amazon.awssdk.core.sync.RequestBody
|
||||
import software.amazon.awssdk.services.s3.S3Client
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest
|
||||
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||
import java.io.InputStream
|
||||
|
||||
@Service
|
||||
class S3StorageService(
|
||||
@@ -22,4 +25,25 @@ class S3StorageService(
|
||||
RequestBody.fromBytes(bytes)
|
||||
)
|
||||
}
|
||||
|
||||
fun findKeyByPrefix(prefix: String): String? {
|
||||
val response = s3Client.listObjectsV2(
|
||||
ListObjectsV2Request.builder()
|
||||
.bucket(bucket)
|
||||
.prefix(prefix)
|
||||
.maxKeys(1)
|
||||
.build()
|
||||
)
|
||||
return response.contents().firstOrNull()?.key()
|
||||
}
|
||||
|
||||
fun download(key: String): Pair<InputStream, String> {
|
||||
val response = s3Client.getObject(
|
||||
GetObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.build()
|
||||
)
|
||||
return Pair(response, response.response().contentType())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user