Files
TrisolarisServer/src/main/kotlin/com/android/trisolarisserver/component/RoomImageStorage.kt
androidlover5842 9b64b34ab9
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
Deduplicate logic across controllers, auth, and schema fixes
2026-01-28 23:03:48 +05:30

112 lines
4.1 KiB
Kotlin

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
)