Migrate to ImageEndpoint

This commit is contained in:
grimsi
2024-12-22 22:18:00 +01:00
parent a45d8812dc
commit c1012c7e96
9 changed files with 113 additions and 78 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080">
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
<method v="2" />
</configuration>
</component>
@@ -1,6 +1,7 @@
package de.grimsi.gameyfin.core.annotations
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FUNCTION
/**
@@ -9,6 +10,6 @@ import kotlin.annotation.AnnotationTarget.FUNCTION
* One example would be the main library view.
*/
@Target(FUNCTION)
@Target(FUNCTION, CLASS)
@Retention(RUNTIME)
annotation class DynamicPublicAccess
@@ -66,7 +66,7 @@ class GameService(
var game = toEntity(metadata, path, plugin)
game = createOrUpdate(game)
return toDto(game)
}
@@ -100,7 +100,7 @@ class GameService(
keywords = game.keywords.toList(),
features = game.features.map { it.name },
perspectives = game.perspectives.map { it.name },
images = game.images.mapNotNull { it.contentId },
images = game.images.mapNotNull { it.id },
videoUrls = game.videoUrls.map { it.toString() },
source = game.source.pluginId
)
@@ -16,7 +16,7 @@ class GameDto(
val keywords: List<String>?,
val features: List<String>?,
val perspectives: List<String>?,
val images: List<String>?,
val images: List<Long>?,
val videoUrls: List<String>?,
val source: String?
)
@@ -1,60 +0,0 @@
package de.grimsi.gameyfin.media
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.users.UserService
import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed
import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/images")
class ImageController(
private val userService: UserService,
private val imageService: ImageService
) {
@PostMapping("/avatar/upload")
fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
val auth: Authentication = SecurityContextHolder.getContext().authentication
userService.setAvatar(auth.name, file)
}
@PostMapping("/avatar/delete")
fun deleteAvatar() {
val auth: Authentication = SecurityContextHolder.getContext().authentication
userService.deleteAvatar(auth.name)
}
@RolesAllowed(Role.Names.ADMIN)
@PostMapping("/avatar/deleteByName")
fun deleteAvatarByName(@RequestParam("name") name: String) {
userService.deleteAvatar(name)
}
@PermitAll
@GetMapping("/avatar")
fun getAvatar(
@RequestParam("username") username: String
): ResponseEntity<InputStreamResource>? {
val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build()
val file = avatar.let { imageService.getFileContent(it) }
val inputStreamResource = InputStreamResource(file)
val headers = HttpHeaders()
headers.contentLength = avatar.contentLength!!
headers.contentType = MediaType.parseMediaType(avatar.mimeType!!)
return ResponseEntity.ok()
.headers(headers)
.body(inputStreamResource)
}
}
@@ -0,0 +1,86 @@
package de.grimsi.gameyfin.media
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.annotations.DynamicPublicAccess
import de.grimsi.gameyfin.games.entities.Image
import de.grimsi.gameyfin.games.entities.ImageType
import de.grimsi.gameyfin.users.UserService
import jakarta.annotation.security.RolesAllowed
import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
@DynamicPublicAccess
@RestController
@RequestMapping("/images")
class ImageEndpoint(
private val imageService: ImageService,
private val userService: UserService
) {
@GetMapping("/screenshot/{id}")
fun getScreenshot(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
return getImageContent(id)
}
@GetMapping("/cover/{id}")
fun getCover(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
return getImageContent(id)
}
@GetMapping("/avatar")
fun getAvatarByUsername(@RequestParam username: String): ResponseEntity<InputStreamResource>? {
val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build()
if (avatar.id == null) return ResponseEntity.notFound().build()
return getImageContent(avatar.id!!)
}
@PostMapping("/avatar/upload")
fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
val auth: Authentication = SecurityContextHolder.getContext().authentication
val image: Image = if (userService.hasAvatar(auth.name)) {
imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
} else {
val existingAvatar = userService.getAvatar(auth.name)!!
imageService.updateFileContent(existingAvatar, file.inputStream, file.contentType!!)
}
userService.updateAvatar(auth.name, image)
}
@PostMapping("/avatar/delete")
fun deleteAvatar() {
val auth: Authentication = SecurityContextHolder.getContext().authentication
userService.deleteAvatar(auth.name)
}
@RolesAllowed(Role.Names.ADMIN)
@PostMapping("/avatar/deleteByName")
fun deleteAvatarByName(@RequestParam("name") name: String) {
userService.deleteAvatar(name)
}
private fun getImageContent(id: Long): ResponseEntity<InputStreamResource>? {
val image = imageService.getImage(id) ?: return ResponseEntity.notFound().build()
val file = image.let { imageService.getFileContent(it) }
if (file == null) return ResponseEntity.notFound().build()
val inputStreamResource = InputStreamResource(file)
val headers = HttpHeaders()
headers.contentLength = image.contentLength!!
headers.contentType = MediaType.parseMediaType(image.mimeType!!)
return ResponseEntity.ok()
.headers(headers)
.body(inputStreamResource)
}
}
@@ -4,6 +4,7 @@ import de.grimsi.gameyfin.games.entities.Image
import de.grimsi.gameyfin.games.entities.ImageType
import de.grimsi.gameyfin.games.repositories.ImageContentStore
import de.grimsi.gameyfin.games.repositories.ImageRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.io.InputStream
@@ -13,14 +14,24 @@ class ImageService(
private val imageContentStore: ImageContentStore
) {
fun getImage(id: Long): Image? {
return imageRepository.findByIdOrNull(id)
}
fun createFile(type: ImageType, content: InputStream, mimeType: String): Image {
val image = Image(type = type, mimeType = mimeType)
imageRepository.save(image)
return imageContentStore.setContent(image, content)
}
fun getFileContent(image: Image): InputStream {
fun getFileContent(id: Long): InputStream? {
val image = getImage(id) ?: return null
return getFileContent(image)
}
fun getFileContent(image: Image): InputStream? {
return imageContentStore.getContent(image)
}
fun deleteFile(image: Image) {
@@ -6,7 +6,6 @@ import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.Utils
import de.grimsi.gameyfin.core.events.*
import de.grimsi.gameyfin.games.entities.Image
import de.grimsi.gameyfin.games.entities.ImageType
import de.grimsi.gameyfin.media.ImageService
import de.grimsi.gameyfin.users.dto.UserInfoDto
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
@@ -28,7 +27,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
@Service
@@ -105,15 +103,9 @@ class UserService(
return user.avatar
}
fun setAvatar(username: String, file: MultipartFile) {
fun updateAvatar(username: String, newAvatar: Image) {
val user = getByUsernameNonNull(username)
if (user.avatar == null) {
user.avatar = imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
} else {
user.avatar = imageService.updateFileContent(user.avatar!!, file.inputStream, file.contentType!!)
}
user.avatar = newAvatar
userRepository.save(user)
}
@@ -127,6 +119,11 @@ class UserService(
userRepository.save(user)
}
fun hasAvatar(username: String): Boolean {
val user = getByUsernameNonNull(username)
return user.avatar != null && user.avatar!!.id != null
}
fun registerOrUpdateUser(user: User): User {
user.password = passwordEncoder.encode(user.password)
return userRepository.save(user)
@@ -271,7 +268,7 @@ class UserService(
emailConfirmed = user.emailConfirmed,
isEnabled = user.enabled,
hasAvatar = user.avatar != null,
avatarId = user.avatar?.contentId,
avatarId = user.avatar?.id,
managedBySso = user.oidcProviderId != null,
roles = user.roles
)
@@ -9,6 +9,6 @@ data class UserInfoDto(
val emailConfirmed: Boolean,
val isEnabled: Boolean,
val hasAvatar: Boolean,
val avatarId: String? = null,
val avatarId: Long? = null,
var roles: Set<Role>
)