Add duplicate image check and delete endpoint
All checks were successful
build-and-deploy / build-deploy (push) Successful in 28s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 28s
This commit is contained in:
@@ -14,6 +14,7 @@ import org.springframework.http.HttpStatus
|
|||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -25,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile
|
|||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -68,6 +70,10 @@ class RoomImages(
|
|||||||
if (file.isEmpty) {
|
if (file.isEmpty) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
|
||||||
}
|
}
|
||||||
|
val contentHash = sha256Hex(file.bytes)
|
||||||
|
if (roomImageRepo.existsByRoomIdAndContentHash(roomId, contentHash)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Duplicate image for room")
|
||||||
|
}
|
||||||
val stored = try {
|
val stored = try {
|
||||||
storage.store(propertyId, roomId, file)
|
storage.store(propertyId, roomId, file)
|
||||||
} catch (ex: IllegalArgumentException) {
|
} catch (ex: IllegalArgumentException) {
|
||||||
@@ -83,6 +89,7 @@ class RoomImages(
|
|||||||
thumbnailPath = stored.thumbnailPath,
|
thumbnailPath = stored.thumbnailPath,
|
||||||
contentType = stored.contentType,
|
contentType = stored.contentType,
|
||||||
sizeBytes = stored.sizeBytes,
|
sizeBytes = stored.sizeBytes,
|
||||||
|
contentHash = contentHash,
|
||||||
roomTypeCode = room.roomType.code,
|
roomTypeCode = room.roomType.code,
|
||||||
tags = tags?.toMutableSet() ?: mutableSetOf(),
|
tags = tags?.toMutableSet() ?: mutableSetOf(),
|
||||||
roomSortOrder = nextRoomSortOrder,
|
roomSortOrder = nextRoomSortOrder,
|
||||||
@@ -91,6 +98,30 @@ class RoomImages(
|
|||||||
return roomImageRepo.save(image).toResponse(publicBaseUrl)
|
return roomImageRepo.save(image).toResponse(publicBaseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{imageId}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
fun delete(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable roomId: UUID,
|
||||||
|
@PathVariable imageId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
) {
|
||||||
|
requirePrincipal(principal)
|
||||||
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||||
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
||||||
|
ensureRoom(propertyId, roomId)
|
||||||
|
|
||||||
|
val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(image.originalPath))
|
||||||
|
Files.deleteIfExists(Paths.get(image.thumbnailPath))
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete image files")
|
||||||
|
}
|
||||||
|
roomImageRepo.delete(image)
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{imageId}/file")
|
@GetMapping("/{imageId}/file")
|
||||||
fun file(
|
fun file(
|
||||||
@PathVariable propertyId: UUID,
|
@PathVariable propertyId: UUID,
|
||||||
@@ -129,6 +160,16 @@ class RoomImages(
|
|||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sha256Hex(bytes: ByteArray): String {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hash = md.digest(bytes)
|
||||||
|
val sb = StringBuilder(hash.size * 2)
|
||||||
|
for (b in hash) {
|
||||||
|
sb.append(String.format("%02x", b))
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomImage.toResponse(baseUrl: String): RoomImageResponse {
|
private fun RoomImage.toResponse(baseUrl: String): RoomImageResponse {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class RoomImage(
|
|||||||
@Column(name = "size_bytes", nullable = false)
|
@Column(name = "size_bytes", nullable = false)
|
||||||
var sizeBytes: Long,
|
var sizeBytes: Long,
|
||||||
|
|
||||||
|
@Column(name = "content_hash", nullable = false)
|
||||||
|
var contentHash: String,
|
||||||
|
|
||||||
@Column(name = "room_type_code")
|
@Column(name = "room_type_code")
|
||||||
var roomTypeCode: String? = null,
|
var roomTypeCode: String? = null,
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface RoomImageRepo : JpaRepository<RoomImage, UUID> {
|
|||||||
)
|
)
|
||||||
fun findByRoomIdOrdered(@Param("roomId") roomId: UUID): List<RoomImage>
|
fun findByRoomIdOrdered(@Param("roomId") roomId: UUID): List<RoomImage>
|
||||||
fun findByIdAndRoomIdAndPropertyId(id: UUID, roomId: UUID, propertyId: UUID): RoomImage?
|
fun findByIdAndRoomIdAndPropertyId(id: UUID, roomId: UUID, propertyId: UUID): RoomImage?
|
||||||
|
fun existsByRoomIdAndContentHash(roomId: UUID, contentHash: String): Boolean
|
||||||
|
|
||||||
@Query("select coalesce(max(ri.roomSortOrder), 0) from RoomImage ri where ri.room.id = :roomId")
|
@Query("select coalesce(max(ri.roomSortOrder), 0) from RoomImage ri where ri.room.id = :roomId")
|
||||||
fun findMaxRoomSortOrder(@Param("roomId") roomId: UUID): Int
|
fun findMaxRoomSortOrder(@Param("roomId") roomId: UUID): Int
|
||||||
|
|||||||
Reference in New Issue
Block a user