Add duplicate image check and delete endpoint
All checks were successful
build-and-deploy / build-deploy (push) Successful in 28s

This commit is contained in:
androidlover5842
2026-01-27 16:50:56 +05:30
parent 5bfbd295c9
commit cb1b65937e
3 changed files with 45 additions and 0 deletions

View File

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

View File

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

View File

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