Skip to content

Commit

Permalink
Merge pull request #497 from Shikkanime/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Ziedelth authored May 29, 2024
2 parents aa796f5 + 8c04629 commit 47eef40
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/fr/shikkanime/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fun main() {
logger.info("Starting server...")
embeddedServer(
Netty,
port = 37100,
port = Constant.PORT,
host = "0.0.0.0",
module = Application::module
).start(wait = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,7 @@ class AttachmentController {
)
}

val type = ImageService.Type.entries.find { it.name.equals(typeString, true) }

if (type == null) {
return Response.badRequest(
MessageDto(
MessageDto.Type.ERROR,
"Type is required"
)
)
}
val type = ImageService.Type.entries.find { it.name.equals(typeString, true) } ?: ImageService.Type.IMAGE

val image = ImageService[uuid, type] ?: return Response.notFound(
MessageDto(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,16 @@ class MemberActionController {
)
}

memberActionService.validateAction(uuid, action)
return Response.ok()
try {
memberActionService.validateAction(uuid, action)
return Response.ok()
} catch (e: Exception) {
return Response.badRequest(
MessageDto(
MessageDto.Type.ERROR,
e.message ?: "An error occurred"
)
)
}
}
}
44 changes: 44 additions & 0 deletions src/main/kotlin/fr/shikkanime/controllers/api/MemberController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.shikkanime.controllers.api
import com.google.inject.Inject
import fr.shikkanime.dtos.AllFollowedEpisodeDto
import fr.shikkanime.dtos.GenericDto
import fr.shikkanime.services.ImageService
import fr.shikkanime.services.MemberFollowAnimeService
import fr.shikkanime.services.MemberFollowEpisodeService
import fr.shikkanime.services.MemberService
Expand All @@ -15,9 +16,12 @@ import fr.shikkanime.utils.routes.method.Put
import fr.shikkanime.utils.routes.openapi.OpenAPI
import fr.shikkanime.utils.routes.openapi.OpenAPIResponse
import fr.shikkanime.utils.routes.param.BodyParam
import io.ktor.http.content.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.util.*
import javax.imageio.ImageIO

@Controller("/api/v1/members")
class MemberController {
Expand Down Expand Up @@ -194,4 +198,44 @@ class MemberController {
private fun unfollowEpisode(@JWTUser uuidUser: UUID, @BodyParam episode: GenericDto): Response {
return memberFollowEpisodeService.unfollow(uuidUser, episode)
}

@Path("/image")
@Post
@JWTAuthenticated
@OpenAPI(
description = "Upload an profile image",
responses = [
OpenAPIResponse(200, "Profile image uploaded successfully"),
OpenAPIResponse(400, "Invalid file format"),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun uploadProfileImage(@JWTUser uuidUser: UUID, @BodyParam multiPartData: MultiPartData): Response {
val file = runBlocking { multiPartData.readAllParts().filterIsInstance<PartData.FileItem>().firstOrNull() }
?: return Response.badRequest("No file provided")
val bytes = file.streamProvider().readBytes()

try {
val imageInputStream = ImageIO.createImageInputStream(ByteArrayInputStream(bytes))
val imageReaders = ImageIO.getImageReaders(imageInputStream)
require(imageReaders.hasNext()) { "Invalid file format" }
val imageReader = imageReaders.next()
val authorizedFormats = setOf("png", "jpeg", "jpg", "jpe")
require(imageReader.formatName.lowercase() in authorizedFormats) { "Invalid file format, only png and jpeg are allowed. Received ${imageReader.formatName}" }
} catch (e: Exception) {
return Response.badRequest(e.message ?: "Invalid file format")
}

ImageService.add(
uuidUser,
ImageService.Type.IMAGE,
bytes,
128,
128,
true
)

return Response.ok()
}
}
4 changes: 3 additions & 1 deletion src/main/kotlin/fr/shikkanime/jobs/FetchOldEpisodesJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,18 @@ class FetchOldEpisodesJob : AbstractJob {

logger.info("Found ${episodes.size} episodes, saving...")
var realSaved = 0
val realSavedAnimes = mutableSetOf<String>()

val variants = episodes.sortedBy { it.releaseDateTime }.map { episode ->
episodeVariantService.findByIdentifier(episode.getIdentifier()) ?: run {
realSavedAnimes.add(episode.anime)
realSaved++
episodeVariantService.save(episode)
}
}

logger.info("Saved $realSaved episodes")
realSavedAnimes.forEach { logger.info("Updating ${StringUtils.getShortName(it)}...") }

if (realSaved > 0) {
logger.info("Updating mappings...")
Expand All @@ -121,7 +124,6 @@ class FetchOldEpisodesJob : AbstractJob {
variants.groupBy { it.mapping!!.anime!!.uuid }.forEach { (animeUuid, _) ->
val anime = animeService.find(animeUuid) ?: return@forEach
val mappingVariants = episodeVariantService.findAllByAnime(anime)
logger.info("Updating ${StringUtils.getShortName(anime.name!!)}...")
anime.releaseDateTime = mappingVariants.minOf { it.releaseDateTime }
anime.lastReleaseDateTime = mappingVariants.maxOf { it.releaseDateTime }
animeService.update(anime)
Expand Down
10 changes: 9 additions & 1 deletion src/main/kotlin/fr/shikkanime/modules/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
import java.time.ZonedDateTime
import java.util.*
import java.util.logging.Level
Expand Down Expand Up @@ -235,7 +236,13 @@ private suspend fun callMethodWithParameters(
}

method.isAccessible = true
return method.callBy(methodParams) as Response

try {
return method.callBy(methodParams) as Response
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error while calling method $method", e)
return Response.internalServerError()
}
}

private suspend fun handleBodyParam(kParameter: KParameter, call: ApplicationCall): Any {
Expand All @@ -246,6 +253,7 @@ private suspend fun handleBodyParam(kParameter: KParameter, call: ApplicationCal
AnimeDto::class.java -> call.receive<AnimeDto>()
EpisodeMappingDto::class.java -> call.receive<EpisodeMappingDto>()
GenericDto::class.java -> call.receive<GenericDto>()
MultiPartData::class.java -> call.receiveMultipart()
else -> call.receive<String>()
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/main/kotlin/fr/shikkanime/modules/SwaggerRouting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor
import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute
import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody
import io.ktor.http.*
import io.ktor.http.content.*
import java.io.File
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.jvmErasure

fun swagger(
Expand Down Expand Up @@ -60,7 +63,17 @@ private fun OpenApiRoute.swaggerRequest(method: KFunction<*>) {
}

parameter.hasAnnotation<BodyParam>() -> {
body(type)
if (parameter.type.javaType == MultiPartData::class.java) {
multipartBody {
description = "Multipart data"
mediaType(ContentType.MultiPart.FormData)
part<File>("file") {
mediaTypes = listOf(ContentType.Image.PNG, ContentType.Image.JPEG)
}
}
} else {
body(type)
}
}
}
}
Expand Down
62 changes: 54 additions & 8 deletions src/main/kotlin/fr/shikkanime/services/ImageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import kotlin.math.min
import kotlin.math.pow
import kotlin.system.measureTimeMillis

private const val FAILED_TO_ENCODE_MESSAGE = "Failed to encode image to WebP"

object ImageService {
enum class Type {
IMAGE,
Expand All @@ -32,7 +34,7 @@ object ImageService {
data class Image(
val uuid: String,
val type: Type,
val url: String,
val url: String? = null,
var bytes: ByteArray = byteArrayOf(),
var originalSize: Long = 0,
var size: Long = 0,
Expand Down Expand Up @@ -182,21 +184,65 @@ object ImageService {
threadPool.submit { encodeImage(url, uuid, type, width, height, image) }
}

fun add(uuid: UUID, type: Type, bytes: ByteArray, width: Int, height: Int, bypass: Boolean = false) {
if (!bypass && (get(uuid, type) != null || bytes.isEmpty())) {
return
}

val image = if (!bypass) {
val image = Image(uuid.toString(), type)
cache.add(image)
image
} else {
get(uuid, type) ?: run {
val image = Image(uuid.toString(), type)
cache.add(image)
image
}
}

threadPool.submit { encodeImage(bytes, uuid, type, width, height, image) }
}

private fun encodeImage(
url: String,
uuid: UUID,
type: Type,
width: Int,
height: Int,
image: Image
) {
val httpResponse = runBlocking { HttpRequest().get(url) }
val bytes = runBlocking { httpResponse.readBytes() }

if (httpResponse.status != HttpStatusCode.OK || bytes.isEmpty()) {
logger.warning("Failed to load image $url")
remove(uuid, type)
return
}

encodeImage(
bytes,
uuid,
type,
width,
height,
image
)
}

private fun encodeImage(
bytes: ByteArray,
uuid: UUID,
type: Type,
width: Int,
height: Int,
image: Image
) {
val take = measureTimeMillis {
try {
val httpResponse = runBlocking { HttpRequest().get(url) }
val bytes = runBlocking { httpResponse.readBytes() }

if (httpResponse.status != HttpStatusCode.OK || bytes.isEmpty()) {
logger.warning("Failed to load image $url")
if (bytes.isEmpty()) {
logger.warning(FAILED_TO_ENCODE_MESSAGE)
remove(uuid, type)
return@measureTimeMillis
}
Expand All @@ -211,7 +257,7 @@ object ImageService {
logger.warning("Can not delete tmp file image")

if (webp.isEmpty()) {
logger.warning("Failed to encode image to WebP")
logger.warning(FAILED_TO_ENCODE_MESSAGE)
remove(uuid, type)
return@measureTimeMillis
}
Expand All @@ -230,7 +276,7 @@ object ImageService {
cache[indexOf] = image
change.set(true)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Failed to load image $url", e)
logger.log(Level.SEVERE, FAILED_TO_ENCODE_MESSAGE, e)
remove(uuid, type)
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/fr/shikkanime/utils/Constant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import java.time.ZoneId

object Constant {
const val NAME = "Shikkanime"
const val PORT = 37100

val reflections = Reflections("fr.shikkanime")
val injector: Injector = Guice.createInjector(DefaultModule())
val abstractPlatforms = reflections.getSubTypesOf(AbstractPlatform::class.java).map { injector.getInstance(it) }
Expand All @@ -38,8 +40,8 @@ object Constant {
val jwtDomain: String = System.getenv("JWT_DOMAIN") ?: "https://jwt-provider-domain/"
val jwtRealm: String = System.getenv("JWT_REALM") ?: "ktor sample app"
val jwtSecret: String = System.getenv("JWT_SECRET") ?: "secret"
val apiUrl: String = System.getenv("API_URL") ?: "http://localhost:37100/api"
val baseUrl: String = System.getenv("BASE_URL") ?: "http://localhost:37100"
val apiUrl: String = System.getenv("API_URL") ?: "http://localhost:$PORT/api"
val baseUrl: String = System.getenv("BASE_URL") ?: "http://localhost:$PORT"
val DEFAULT_IMAGE_PREVIEW = "$baseUrl/assets/img/episode_no_image_preview.jpg"
const val DEFAULT_CACHE_DURATION = 31536000 // 1 year

Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/fr/shikkanime/utils/routes/Response.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,8 @@ open class Response(

fun conflict(data: Any? = null, session: TokenDto? = null): Response =
Response(HttpStatusCode.Conflict, data = data, session = session)

fun internalServerError(): Response =
Response(HttpStatusCode.InternalServerError)
}
}
Loading

0 comments on commit 47eef40

Please sign in to comment.