From a45d8812dcbf235113c420670ada73bd4f2317ec Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:09:02 +0100 Subject: [PATCH] Move AvatarController to ImageController --- .../main/frontend/endpoints/AvatarEndpoint.ts | 6 ++-- gameyfin/src/main/frontend/views/TestView.tsx | 4 +-- .../grimsi/gameyfin/games/entities/Image.kt | 5 +-- .../ImageController.kt} | 21 +++++------ .../de/grimsi/gameyfin/media/ImageService.kt | 36 +++++++++++++++++++ .../de/grimsi/gameyfin/users/UserService.kt | 23 ++++++------ .../grimsi/gameyfin/users/dto/UserInfoDto.kt | 1 + .../grimsi/gameyfin/users/entities/Avatar.kt | 23 ------------ .../de/grimsi/gameyfin/users/entities/User.kt | 6 ++-- .../users/persistence/AvatarContentStore.kt | 8 ----- 10 files changed, 67 insertions(+), 66 deletions(-) rename gameyfin/src/main/kotlin/de/grimsi/gameyfin/{users/avatar/AvatarController.kt => media/ImageController.kt} (74%) create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt delete mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt delete mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt diff --git a/gameyfin/src/main/frontend/endpoints/AvatarEndpoint.ts b/gameyfin/src/main/frontend/endpoints/AvatarEndpoint.ts index 9d484bb..a1b8309 100644 --- a/gameyfin/src/main/frontend/endpoints/AvatarEndpoint.ts +++ b/gameyfin/src/main/frontend/endpoints/AvatarEndpoint.ts @@ -5,7 +5,7 @@ export async function uploadAvatar(avatar: any) { const formData = new FormData(); formData.append("file", avatar); - const response = await fetchWithAuth("avatar/upload", formData); + const response = await fetchWithAuth("images/avatar/upload", formData); const result = await response.text(); @@ -17,7 +17,7 @@ export async function uploadAvatar(avatar: any) { } export async function removeAvatar() { - const response = await fetchWithAuth("avatar/delete") + const response = await fetchWithAuth("images/avatar/delete") const result = await response.text(); @@ -29,7 +29,7 @@ export async function removeAvatar() { } export async function removeAvatarByName(name: string) { - const response = await fetchWithAuth("avatar/deleteByName?" + new URLSearchParams({name: name})) + const response = await fetchWithAuth("images/avatar/deleteByName?" + new URLSearchParams({name: name})) const result = await response.text(); diff --git a/gameyfin/src/main/frontend/views/TestView.tsx b/gameyfin/src/main/frontend/views/TestView.tsx index e284a50..800feb2 100644 --- a/gameyfin/src/main/frontend/views/TestView.tsx +++ b/gameyfin/src/main/frontend/views/TestView.tsx @@ -3,11 +3,11 @@ import {Button, Input} from "@nextui-org/react"; import {toast} from "sonner"; import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints"; import {useState} from "react"; -import Game from "Frontend/generated/de/grimsi/gameyfin/games/Game"; +import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto"; export default function TestView() { const [gameTitle, setGameTitle] = useState(""); - const [game, setGame] = useState(); + const [game, setGame] = useState(); function getGame() { LibraryEndpoint.test(gameTitle).then(game => { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Image.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Image.kt index f0a8acf..f1056b5 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Image.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Image.kt @@ -16,7 +16,7 @@ class Image( @GeneratedValue(strategy = GenerationType.AUTO) var id: Long? = null, - val originalUrl: URL, + val originalUrl: URL? = null, val type: ImageType, @@ -35,5 +35,6 @@ class Image( enum class ImageType { COVER, - SCREENSHOT + SCREENSHOT, + AVATAR } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/avatar/AvatarController.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt similarity index 74% rename from gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/avatar/AvatarController.kt rename to gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt index 800c9ae..2b6f979 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/avatar/AvatarController.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageController.kt @@ -1,26 +1,24 @@ -package de.grimsi.gameyfin.users.avatar +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 jakarta.servlet.http.HttpServletResponse 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.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @RestController -class AvatarController( - private val userService: UserService +@RequestMapping("/images") +class ImageController( + private val userService: UserService, + private val imageService: ImageService ) { @PostMapping("/avatar/upload") @@ -42,14 +40,13 @@ class AvatarController( } @PermitAll - @GetMapping("/images/avatar") + @GetMapping("/avatar") fun getAvatar( - @RequestParam("username") username: String, - response: HttpServletResponse + @RequestParam("username") username: String ): ResponseEntity? { val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build() - val file = avatar.let { userService.getAvatarFile(it) } + val file = avatar.let { imageService.getFileContent(it) } val inputStreamResource = InputStreamResource(file) val headers = HttpHeaders() diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt new file mode 100644 index 0000000..61c9f6f --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageService.kt @@ -0,0 +1,36 @@ +package de.grimsi.gameyfin.media + +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.stereotype.Service +import java.io.InputStream + +@Service +class ImageService( + private val imageRepository: ImageRepository, + private val imageContentStore: ImageContentStore +) { + + 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 { + return imageContentStore.getContent(image) + } + + fun deleteFile(image: Image) { + imageContentStore.unsetContent(image) + imageRepository.delete(image) + } + + fun updateFileContent(image: Image, content: InputStream, mimeType: String? = null): Image { + mimeType?.let { image.mimeType = it } + imageRepository.save(image) + return imageContentStore.setContent(image, content) + } +} \ No newline at end of file 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 4739d55..14a36b8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt @@ -5,14 +5,15 @@ import de.grimsi.gameyfin.config.ConfigService 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 import de.grimsi.gameyfin.users.dto.UserUpdateDto import de.grimsi.gameyfin.users.emailconfirmation.EmailConfirmationService -import de.grimsi.gameyfin.users.entities.Avatar import de.grimsi.gameyfin.users.entities.User import de.grimsi.gameyfin.users.enums.RoleAssignmentResult -import de.grimsi.gameyfin.users.persistence.AvatarContentStore import de.grimsi.gameyfin.users.persistence.UserRepository import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.transaction.Transactional @@ -28,14 +29,13 @@ 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 -import java.io.InputStream @Service @Transactional class UserService( private val userRepository: UserRepository, - private val avatarStore: AvatarContentStore, + private val imageService: ImageService, private val passwordEncoder: PasswordEncoder, private val roleService: RoleService, private val sessionService: SessionService, @@ -100,23 +100,20 @@ class UserService( return toUserInfo(user) } - fun getAvatar(username: String): Avatar? { + fun getAvatar(username: String): Image? { val user = getByUsernameNonNull(username) return user.avatar } - fun getAvatarFile(avatar: Avatar): InputStream { - return avatarStore.getContent(avatar) - } - fun setAvatar(username: String, file: MultipartFile) { val user = getByUsernameNonNull(username) if (user.avatar == null) { - user.avatar = Avatar(mimeType = file.contentType) + user.avatar = imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!) + } else { + user.avatar = imageService.updateFileContent(user.avatar!!, file.inputStream, file.contentType!!) } - avatarStore.setContent(user.avatar, file.inputStream) userRepository.save(user) } @@ -124,8 +121,7 @@ class UserService( val user = getByUsernameNonNull(username) if (user.avatar == null) return - - avatarStore.unsetContent(user.avatar) + imageService.deleteFile(user.avatar!!) user.avatar = null userRepository.save(user) @@ -275,6 +271,7 @@ class UserService( emailConfirmed = user.emailConfirmed, isEnabled = user.enabled, hasAvatar = user.avatar != null, + avatarId = user.avatar?.contentId, 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 b38a23a..4e9d120 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,5 +9,6 @@ data class UserInfoDto( val emailConfirmed: Boolean, val isEnabled: Boolean, val hasAvatar: Boolean, + val avatarId: String? = null, var roles: Set ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt deleted file mode 100644 index 24877a9..0000000 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.grimsi.gameyfin.users.entities - -import jakarta.annotation.Nullable -import jakarta.persistence.Embeddable -import org.springframework.content.commons.annotations.ContentId -import org.springframework.content.commons.annotations.ContentLength -import org.springframework.content.commons.annotations.MimeType - - -@Embeddable -class Avatar( - @ContentId - @Nullable - var contentId: String? = null, - - @ContentLength - @Nullable - var contentLength: Long? = null, - - @MimeType - @Nullable - var mimeType: String? = null -) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt index a574ded..952365f 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt @@ -2,6 +2,7 @@ package de.grimsi.gameyfin.users.entities import de.grimsi.gameyfin.core.Role import de.grimsi.gameyfin.core.security.EncryptionConverter +import de.grimsi.gameyfin.games.entities.Image import jakarta.annotation.Nullable import jakarta.persistence.* import jakarta.validation.constraints.NotNull @@ -32,9 +33,8 @@ class User( var enabled: Boolean = false, - @Embedded - @Nullable - var avatar: Avatar? = null, + @OneToOne(cascade = [CascadeType.ALL]) + var avatar: Image? = null, @ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt deleted file mode 100644 index f8c1e3c..0000000 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.grimsi.gameyfin.users.persistence - -import de.grimsi.gameyfin.users.entities.Avatar -import org.springframework.content.commons.store.ContentStore -import org.springframework.stereotype.Repository - -@Repository -interface AvatarContentStore : ContentStore \ No newline at end of file