package com.android.trisolarisserver.component import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.nio.file.Files import java.nio.file.Paths import java.time.OffsetDateTime import java.util.UUID import javax.imageio.ImageIO @Component class RoomImageStorage( @Value("\${storage.rooms.root:/home/androidlover5842/docs/rooms}") private val rootPath: String ) { init { val root = Paths.get(rootPath) try { Files.createDirectories(root) } catch (ex: Exception) { throw IllegalStateException("Room image root not writable: $rootPath", ex) } if (!Files.isWritable(root)) { throw IllegalStateException("Room image root not writable: $rootPath") } } fun store(propertyId: UUID, roomId: UUID, file: MultipartFile): StoredRoomImage { val contentType = file.contentType ?: "" if (!contentType.startsWith("image/")) { throw IllegalArgumentException("Only image files are allowed") } val bytes = file.bytes val originalName = file.originalFilename ?: UUID.randomUUID().toString() val ext = extensionFor(contentType, originalName) val dir = Paths.get(rootPath, propertyId.toString(), roomId.toString()) try { Files.createDirectories(dir) } catch (ex: Exception) { throw IllegalStateException("Failed to create room image directory: $dir", ex) } val base = UUID.randomUUID().toString() + "_" + OffsetDateTime.now().toEpochSecond() val originalPath = dir.resolve("$base.$ext") val originalTmp = dir.resolve("$base.$ext.tmp") Files.write(originalTmp, bytes) atomicMove(originalTmp, originalPath) val image = readImage(bytes) ?: throw IllegalArgumentException("Unsupported image") val thumb = resize(image, 320) val thumbExt = if (ext.lowercase() == "jpg") "jpg" else "png" val thumbPath = dir.resolve("${base}_thumb.$thumbExt") val thumbTmp = dir.resolve("${base}_thumb.$thumbExt.tmp") ByteArrayInputStream(render(thumb, thumbExt)).use { input -> Files.copy(input, thumbTmp) } atomicMove(thumbTmp, thumbPath) return StoredRoomImage( originalPath = originalPath.toString(), thumbnailPath = thumbPath.toString(), contentType = contentType, sizeBytes = bytes.size.toLong() ) } private fun readImage(bytes: ByteArray): BufferedImage? { return ByteArrayInputStream(bytes).use { input -> ImageIO.read(input) } } private fun resize(input: BufferedImage, maxSize: Int): BufferedImage { val width = input.width val height = input.height if (width <= maxSize && height <= maxSize) return input val scale = if (width > height) maxSize.toDouble() / width else maxSize.toDouble() / height val newW = (width * scale).toInt() val newH = (height * scale).toInt() val output = BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB) val g = output.createGraphics() g.drawImage(input, 0, 0, newW, newH, null) g.dispose() return output } private fun render(image: BufferedImage, format: String): ByteArray { val out = java.io.ByteArrayOutputStream() ImageIO.write(image, format, out) return out.toByteArray() } private fun extensionFor(contentType: String, filename: String): String { return when { contentType.contains("png", true) -> "png" contentType.contains("jpeg", true) || contentType.contains("jpg", true) -> "jpg" filename.contains(".") -> filename.substringAfterLast('.').lowercase() else -> "png" } } } data class StoredRoomImage( val originalPath: String, val thumbnailPath: String, val contentType: String, val sizeBytes: Long )