Adds image retrieval endpoint

This commit is contained in:
2026-05-14 14:29:52 -05:00
parent 63e015400b
commit aff0872e38
8 changed files with 121 additions and 11 deletions

View File

@@ -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))

View File

@@ -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())
}
}