From cb1b65937ec3224aad67551a4f42b304afdcac45 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Tue, 27 Jan 2026 16:50:56 +0530 Subject: [PATCH] Add duplicate image check and delete endpoint --- .../trisolarisserver/controller/RoomImages.kt | 41 +++++++++++++++++++ .../trisolarisserver/models/room/RoomImage.kt | 3 ++ .../trisolarisserver/repo/RoomImageRepo.kt | 1 + 3 files changed, 45 insertions(+) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt index b5cc21e..3874bb5 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt @@ -14,6 +14,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity 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.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -25,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import java.nio.file.Files import java.nio.file.Paths +import java.security.MessageDigest import java.util.UUID @RestController @@ -68,6 +70,10 @@ class RoomImages( if (file.isEmpty) { 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 { storage.store(propertyId, roomId, file) } catch (ex: IllegalArgumentException) { @@ -83,6 +89,7 @@ class RoomImages( thumbnailPath = stored.thumbnailPath, contentType = stored.contentType, sizeBytes = stored.sizeBytes, + contentHash = contentHash, roomTypeCode = room.roomType.code, tags = tags?.toMutableSet() ?: mutableSetOf(), roomSortOrder = nextRoomSortOrder, @@ -91,6 +98,30 @@ class RoomImages( 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") fun file( @PathVariable propertyId: UUID, @@ -129,6 +160,16 @@ class RoomImages( 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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/models/room/RoomImage.kt b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomImage.kt index 5a02212..d8f3aac 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/room/RoomImage.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomImage.kt @@ -33,6 +33,9 @@ class RoomImage( @Column(name = "size_bytes", nullable = false) var sizeBytes: Long, + @Column(name = "content_hash", nullable = false) + var contentHash: String, + @Column(name = "room_type_code") var roomTypeCode: String? = null, diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RoomImageRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RoomImageRepo.kt index 72c0895..6912525 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/RoomImageRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RoomImageRepo.kt @@ -17,6 +17,7 @@ interface RoomImageRepo : JpaRepository { ) fun findByRoomIdOrdered(@Param("roomId") roomId: UUID): List 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") fun findMaxRoomSortOrder(@Param("roomId") roomId: UUID): Int