diff --git a/.run/UI debug.run.xml b/.run/UI debug.run.xml index 1a2afd9..ccfbe55 100644 --- a/.run/UI debug.run.xml +++ b/.run/UI debug.run.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/annotations/DynamicPublicAccess.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/annotations/DynamicPublicAccess.kt index 2c4b6b7..92076a3 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/annotations/DynamicPublicAccess.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/annotations/DynamicPublicAccess.kt @@ -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 \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt index c0063f7..6784c34 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -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 ) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt index e4e8905..3e5d52f 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt @@ -16,7 +16,7 @@ class GameDto( val keywords: List?, val features: List?, val perspectives: List?, - val images: List?, + val images: List?, val videoUrls: List?, val source: String? ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt deleted file mode 100644 index 2b6f979..0000000 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt +++ /dev/null @@ -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? { - 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) - } -} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt new file mode 100644 index 0000000..38aef29 --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt @@ -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? { + return getImageContent(id) + } + + @GetMapping("/cover/{id}") + fun getCover(@PathVariable("id") id: Long): ResponseEntity? { + return getImageContent(id) + } + + @GetMapping("/avatar") + fun getAvatarByUsername(@RequestParam username: String): ResponseEntity? { + 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? { + 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) + } +} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt index 61c9f6f..c6977a9 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt @@ -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) { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt index 14a36b8..5f5ede5 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt @@ -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 ) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt index 4e9d120..3e68a5c 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt @@ -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 ) \ No newline at end of file