Move AvatarController to ImageController

This commit is contained in:
grimsi
2024-12-22 13:09:02 +01:00
parent eb14007190
commit a45d8812dc
10 changed files with 67 additions and 66 deletions
@@ -5,7 +5,7 @@ export async function uploadAvatar(avatar: any) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", avatar); formData.append("file", avatar);
const response = await fetchWithAuth("avatar/upload", formData); const response = await fetchWithAuth("images/avatar/upload", formData);
const result = await response.text(); const result = await response.text();
@@ -17,7 +17,7 @@ export async function uploadAvatar(avatar: any) {
} }
export async function removeAvatar() { export async function removeAvatar() {
const response = await fetchWithAuth("avatar/delete") const response = await fetchWithAuth("images/avatar/delete")
const result = await response.text(); const result = await response.text();
@@ -29,7 +29,7 @@ export async function removeAvatar() {
} }
export async function removeAvatarByName(name: string) { 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(); const result = await response.text();
@@ -3,11 +3,11 @@ import {Button, Input} from "@nextui-org/react";
import {toast} from "sonner"; import {toast} from "sonner";
import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints"; import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints";
import {useState} from "react"; 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() { export default function TestView() {
const [gameTitle, setGameTitle] = useState(""); const [gameTitle, setGameTitle] = useState("");
const [game, setGame] = useState<Game>(); const [game, setGame] = useState<GameDto>();
function getGame() { function getGame() {
LibraryEndpoint.test(gameTitle).then(game => { LibraryEndpoint.test(gameTitle).then(game => {
@@ -16,7 +16,7 @@ class Image(
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null, var id: Long? = null,
val originalUrl: URL, val originalUrl: URL? = null,
val type: ImageType, val type: ImageType,
@@ -35,5 +35,6 @@ class Image(
enum class ImageType { enum class ImageType {
COVER, COVER,
SCREENSHOT SCREENSHOT,
AVATAR
} }
@@ -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.core.Role
import de.grimsi.gameyfin.users.UserService import de.grimsi.gameyfin.users.UserService
import jakarta.annotation.security.PermitAll import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed import jakarta.annotation.security.RolesAllowed
import jakarta.servlet.http.HttpServletResponse
import org.springframework.core.io.InputStreamResource import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.*
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.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
@RestController @RestController
class AvatarController( @RequestMapping("/images")
private val userService: UserService class ImageController(
private val userService: UserService,
private val imageService: ImageService
) { ) {
@PostMapping("/avatar/upload") @PostMapping("/avatar/upload")
@@ -42,14 +40,13 @@ class AvatarController(
} }
@PermitAll @PermitAll
@GetMapping("/images/avatar") @GetMapping("/avatar")
fun getAvatar( fun getAvatar(
@RequestParam("username") username: String, @RequestParam("username") username: String
response: HttpServletResponse
): ResponseEntity<InputStreamResource>? { ): ResponseEntity<InputStreamResource>? {
val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build() 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 inputStreamResource = InputStreamResource(file)
val headers = HttpHeaders() val headers = HttpHeaders()
@@ -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)
}
}
@@ -5,14 +5,15 @@ import de.grimsi.gameyfin.config.ConfigService
import de.grimsi.gameyfin.core.Role import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.Utils import de.grimsi.gameyfin.core.Utils
import de.grimsi.gameyfin.core.events.* 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.UserInfoDto
import de.grimsi.gameyfin.users.dto.UserRegistrationDto import de.grimsi.gameyfin.users.dto.UserRegistrationDto
import de.grimsi.gameyfin.users.dto.UserUpdateDto import de.grimsi.gameyfin.users.dto.UserUpdateDto
import de.grimsi.gameyfin.users.emailconfirmation.EmailConfirmationService 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.entities.User
import de.grimsi.gameyfin.users.enums.RoleAssignmentResult import de.grimsi.gameyfin.users.enums.RoleAssignmentResult
import de.grimsi.gameyfin.users.persistence.AvatarContentStore
import de.grimsi.gameyfin.users.persistence.UserRepository import de.grimsi.gameyfin.users.persistence.UserRepository
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.transaction.Transactional 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.security.oauth2.core.oidc.user.OidcUser
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.io.InputStream
@Service @Service
@Transactional @Transactional
class UserService( class UserService(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val avatarStore: AvatarContentStore, private val imageService: ImageService,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder,
private val roleService: RoleService, private val roleService: RoleService,
private val sessionService: SessionService, private val sessionService: SessionService,
@@ -100,23 +100,20 @@ class UserService(
return toUserInfo(user) return toUserInfo(user)
} }
fun getAvatar(username: String): Avatar? { fun getAvatar(username: String): Image? {
val user = getByUsernameNonNull(username) val user = getByUsernameNonNull(username)
return user.avatar return user.avatar
} }
fun getAvatarFile(avatar: Avatar): InputStream {
return avatarStore.getContent(avatar)
}
fun setAvatar(username: String, file: MultipartFile) { fun setAvatar(username: String, file: MultipartFile) {
val user = getByUsernameNonNull(username) val user = getByUsernameNonNull(username)
if (user.avatar == null) { 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) userRepository.save(user)
} }
@@ -124,8 +121,7 @@ class UserService(
val user = getByUsernameNonNull(username) val user = getByUsernameNonNull(username)
if (user.avatar == null) return if (user.avatar == null) return
imageService.deleteFile(user.avatar!!)
avatarStore.unsetContent(user.avatar)
user.avatar = null user.avatar = null
userRepository.save(user) userRepository.save(user)
@@ -275,6 +271,7 @@ class UserService(
emailConfirmed = user.emailConfirmed, emailConfirmed = user.emailConfirmed,
isEnabled = user.enabled, isEnabled = user.enabled,
hasAvatar = user.avatar != null, hasAvatar = user.avatar != null,
avatarId = user.avatar?.contentId,
managedBySso = user.oidcProviderId != null, managedBySso = user.oidcProviderId != null,
roles = user.roles roles = user.roles
) )
@@ -9,5 +9,6 @@ data class UserInfoDto(
val emailConfirmed: Boolean, val emailConfirmed: Boolean,
val isEnabled: Boolean, val isEnabled: Boolean,
val hasAvatar: Boolean, val hasAvatar: Boolean,
val avatarId: String? = null,
var roles: Set<Role> var roles: Set<Role>
) )
@@ -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
)
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.users.entities
import de.grimsi.gameyfin.core.Role import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.security.EncryptionConverter import de.grimsi.gameyfin.core.security.EncryptionConverter
import de.grimsi.gameyfin.games.entities.Image
import jakarta.annotation.Nullable import jakarta.annotation.Nullable
import jakarta.persistence.* import jakarta.persistence.*
import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.NotNull
@@ -32,9 +33,8 @@ class User(
var enabled: Boolean = false, var enabled: Boolean = false,
@Embedded @OneToOne(cascade = [CascadeType.ALL])
@Nullable var avatar: Image? = null,
var avatar: Avatar? = null,
@ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER) @ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@@ -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<Avatar, String>