221 lines
9.7 KiB
Kotlin
221 lines
9.7 KiB
Kotlin
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.error.exception.ConflictException
|
|
import net.halfbinary.scavengerhuntapi.error.exception.ForbiddenException
|
|
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
|
|
import net.halfbinary.scavengerhuntapi.model.FoundStatus
|
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
|
import net.halfbinary.scavengerhuntapi.model.ImageVersion
|
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
|
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
|
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
|
import net.halfbinary.scavengerhuntapi.model.TeamId
|
|
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.domain.PhotoFile
|
|
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
|
|
import java.io.ByteArrayInputStream
|
|
import java.io.ByteArrayOutputStream
|
|
import java.time.OffsetDateTime
|
|
|
|
private const val PHOTO_NOT_FOUND = "Photo not found"
|
|
|
|
@Service
|
|
class PhotoService(
|
|
private val photoRepository: PhotoRepository,
|
|
private val hunterService: HunterService,
|
|
private val teamService: TeamService,
|
|
private val huntService: HuntService,
|
|
private val s3StorageService: S3StorageService,
|
|
private val fileProbeService: FileProbeService
|
|
) {
|
|
fun submitPhoto(huntId: HuntId, itemId: ItemId, email: String, file: MultipartFile) {
|
|
val hunter = hunterService.getHunterByEmail(email)
|
|
val hunt = huntService.getHunt(huntId)
|
|
if (!hunter.isAdmin && !hunt.isOngoing) throw ForbiddenException()
|
|
|
|
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 now = OffsetDateTime.now()
|
|
val photo = Photo(
|
|
itemId = itemId,
|
|
huntId = huntId,
|
|
hunterId = hunter.id,
|
|
foundDateTime = now,
|
|
status = PhotoStatus.SUBMITTED,
|
|
statusChangeDateTime = now
|
|
)
|
|
val savedRecord = photoRepository.save(photo.toRecord())
|
|
val baseName = savedRecord.id.toString()
|
|
|
|
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)
|
|
}
|
|
|
|
fun getPhotoInfo(huntId: HuntId, teamId: TeamId, itemId: ItemId, photoId: PhotoId, email: String): PhotoResponse {
|
|
val requestingHunter = hunterService.getHunterByEmail(email)
|
|
val photoRecord = photoRepository.findByIdAndItemIdAndHuntId(photoId, itemId, huntId)
|
|
?: throw NotFoundException(PHOTO_NOT_FOUND)
|
|
|
|
if (!requestingHunter.isAdmin) {
|
|
val hunt = huntService.getHunt(huntId)
|
|
if (!hunt.isOngoing) throw ForbiddenException()
|
|
val team = try {
|
|
teamService.getTeamForHunterInHunt(huntId, email)
|
|
} catch (_: NotFoundException) {
|
|
throw ForbiddenException()
|
|
}
|
|
if (team.id != teamId) throw ForbiddenException()
|
|
}
|
|
|
|
val submitter = hunterService.getHunterById(photoRecord.hunterId)
|
|
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)
|
|
try {
|
|
val requestingTeam =
|
|
teamService.getTeamForHunterInHunt(photoRecord.huntId, requestingHunter.email)
|
|
val submitterTeam =
|
|
teamService.getTeamForHunterInHunt(photoRecord.huntId, submitter.email)
|
|
if (requestingTeam.id != submitterTeam.id) throw ForbiddenException()
|
|
} catch (_: NotFoundException) {
|
|
throw ForbiddenException()
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
fun getItemFoundStatus(huntId: HuntId, teamId: TeamId, itemId: ItemId, email: String): FoundStatus {
|
|
val requestingHunter = hunterService.getHunterByEmail(email)
|
|
|
|
if (!requestingHunter.isAdmin) {
|
|
val hunt = huntService.getHunt(huntId)
|
|
if (!hunt.isOngoing) throw ForbiddenException()
|
|
val team = try {
|
|
teamService.getTeamForHunterInHunt(huntId, email)
|
|
} catch (_: NotFoundException) {
|
|
throw ForbiddenException()
|
|
}
|
|
if (team.id != teamId) throw ForbiddenException()
|
|
}
|
|
|
|
val teamHunterIds = teamService.getHunterIdsForTeam(teamId)
|
|
val photos = photoRepository.findByHuntIdAndItemId(huntId, itemId)
|
|
.filter { it.hunterId in teamHunterIds && it.status != PhotoStatus.REMOVED }
|
|
|
|
return when {
|
|
photos.any { it.status == PhotoStatus.APPROVED } -> FoundStatus.APPROVED
|
|
photos.any { it.status == PhotoStatus.REJECTED } -> FoundStatus.REJECTED
|
|
photos.any { it.status == PhotoStatus.SUBMITTED } -> FoundStatus.SUBMITTED
|
|
else -> FoundStatus.NOT_FOUND
|
|
}
|
|
}
|
|
|
|
fun removePhoto(huntId: HuntId, teamId: TeamId, itemId: ItemId, photoId: PhotoId, email: String) {
|
|
val requestingHunter = hunterService.getHunterByEmail(email)
|
|
|
|
if (!requestingHunter.isAdmin) {
|
|
val hunt = huntService.getHunt(huntId)
|
|
if (!hunt.isOngoing) throw ForbiddenException()
|
|
}
|
|
|
|
val photoRecord = photoRepository.findByIdAndItemIdAndHuntId(photoId, itemId, huntId)
|
|
?: throw NotFoundException(PHOTO_NOT_FOUND)
|
|
|
|
if (!requestingHunter.isAdmin) {
|
|
val team = try {
|
|
teamService.getTeamForHunterInHunt(huntId, email)
|
|
} catch (_: NotFoundException) {
|
|
throw ForbiddenException()
|
|
}
|
|
if (team.id != teamId) throw ForbiddenException()
|
|
}
|
|
|
|
if (photoRecord.status == PhotoStatus.APPROVED) throw ConflictException("Cannot remove an approved photo")
|
|
|
|
photoRepository.save(photoRecord.copy(status = PhotoStatus.REMOVED, statusChangeDateTime = OffsetDateTime.now()))
|
|
}
|
|
|
|
fun getItemPhotos(huntId: HuntId, teamId: TeamId, itemId: ItemId, email: String): List<PhotoResponse> {
|
|
val requestingHunter = hunterService.getHunterByEmail(email)
|
|
|
|
if (!requestingHunter.isAdmin) {
|
|
val hunt = huntService.getHunt(huntId)
|
|
if (!hunt.isOngoing) throw ForbiddenException()
|
|
val team = try {
|
|
teamService.getTeamForHunterInHunt(huntId, email)
|
|
} catch (_: NotFoundException) {
|
|
throw ForbiddenException()
|
|
}
|
|
if (team.id != teamId) throw ForbiddenException()
|
|
}
|
|
|
|
val teamHunterIds = teamService.getHunterIdsForTeam(teamId)
|
|
return photoRepository.findByHuntIdAndItemId(huntId, itemId)
|
|
.filter { it.hunterId in teamHunterIds && it.status != PhotoStatus.REMOVED }
|
|
.map { it.toDomain().toResponse(hunterService.getHunterById(it.hunterId)) }
|
|
}
|
|
|
|
fun updatePhotoStatus(photoId: PhotoId, status: PhotoStatus) {
|
|
val record = photoRepository.findByIdOrNull(photoId)
|
|
?: throw NotFoundException(PHOTO_NOT_FOUND)
|
|
photoRepository.save(record.copy(status = status, statusChangeDateTime = OffsetDateTime.now()))
|
|
}
|
|
|
|
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))
|
|
.width(width)
|
|
.outputFormat("jpg")
|
|
.toOutputStream(output)
|
|
return output.toByteArray()
|
|
}
|
|
}
|