Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] TT-39 - Connect with Google Cloud storage #1

Merged
merged 15 commits into from
Jun 18, 2024
Merged
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
local.properties
!**/src/main/**/build/
!**/src/test/**/build/

Expand Down Expand Up @@ -33,4 +34,4 @@ out/
/.nb-gradle/

### VS Code ###
.vscode/
.vscode/
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("com.google.cloud:google-cloud-storage:2.38.0")

testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
13 changes: 6 additions & 7 deletions src/main/kotlin/com/cheerz/mediamanager/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ package com.cheerz.mediamanager

import com.cheerz.mediamanager.plugins.configureRouting
import com.cheerz.mediamanager.plugins.configureSerialization
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import com.google.cloud.storage.StorageOptions
import io.ktor.server.application.Application
import io.ktor.server.netty.EngineMain

fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
val storage = StorageOptions.getDefaultInstance().service
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Il faudra qu'on regarde pour bien gérer le storage.


public fun main(args: Array<String>): Unit = EngineMain.main(args)

fun Application.module() {
configureSerialization()
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/com/cheerz/mediamanager/models/CheerzResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.cheerz.mediamanager.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CheerzResponse<Payload>(
@SerialName("response") val response: Payload,
@SerialName("error_message") val errorMessage: String?,
)
13 changes: 10 additions & 3 deletions src/main/kotlin/com/cheerz/mediamanager/models/MediaItem.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.cheerz.mediamanager.models

import com.google.cloud.storage.Blob
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MediaItem(
val id: String,
val type: MediaType
@SerialName("id") val id: String,
@SerialName("type") val type: MediaType,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐 Il faudrait ajouter l'uri pour la renvoyer dans la réponse API

)

enum class MediaType { IMAGE, VIDEO }
fun Blob.toMediaItem() = MediaItem(name, MediaType.IMAGE)

enum class MediaType { IMAGE, VIDEO }
9 changes: 3 additions & 6 deletions src/main/kotlin/com/cheerz/mediamanager/plugins/Routing.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package com.cheerz.mediamanager.plugins

import com.cheerz.mediamanager.routes.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.application.Application
import io.ktor.server.routing.routing

fun Application.configureRouting() {
routing {
homeRoute()

listMediaRoute()
getMediaRoute()

mediaRoutes()
testSerializationRoute()
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.cheerz.mediamanager.plugins

import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation

fun Application.configureSerialization() {
install(ContentNegotiation) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.cheerz.mediamanager.routes

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get

fun Route.testSerializationRoute() {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}
9 changes: 5 additions & 4 deletions src/main/kotlin/com/cheerz/mediamanager/routes/Home.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.cheerz.mediamanager.routes

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.get

fun Route.homeRoute() {
get("/") {
call.respondText("Hello World!")
}
}
}
105 changes: 88 additions & 17 deletions src/main/kotlin/com/cheerz/mediamanager/routes/MediaRoutes.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,97 @@
package com.cheerz.mediamanager.routes

import com.cheerz.mediamanager.storage.mediaStorage
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import com.cheerz.mediamanager.models.CheerzResponse
import com.cheerz.mediamanager.models.MediaItem
import com.cheerz.mediamanager.models.MediaType
import com.cheerz.mediamanager.models.toMediaItem
import com.cheerz.mediamanager.storage
import com.google.cloud.storage.Storage
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
import io.ktor.server.application.call
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondFile
import io.ktor.server.routing.*
import java.io.File

fun Route.listMediaRoute() {
get("/media") {
if (mediaStorage.isNotEmpty()) {
call.respond(mediaStorage)
}

fun Route.mediaRoutes() = route("/media") {
upload()
download()
getAll()
}

private fun Route.getAll() {
get {
val bucketName = call.application.environment.config.property("ktor.storage.bucket_name").getString()
val buckets = storage.get(bucketName).list(Storage.BlobListOption.currentDirectory()).values
val medias = buckets
.filter { it.contentType?.startsWith("image") == true }
.map { it.toMediaItem() }

call.response.status(HttpStatusCode.OK)
call.respond(
CheerzResponse(medias, null)
)
}
}

fun Route.getMediaRoute() {
get("/media/{id?}") {
val id = call.parameters["id"] ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)
val media = mediaStorage.find { it.id == id } ?: return@get call.respondText(
"Not Found",
status = HttpStatusCode.NotFound
private fun Route.upload() {
post("/upload") {
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val uuid = java.util.UUID.randomUUID().toString()
val fileBytes = part.streamProvider().readBytes()

val bucketName = call.application.environment.config.property("ktor.storage.bucket_name").getString()
storage.get(bucketName).create(uuid, fileBytes, part.contentType.toString())

val media = MediaItem(uuid, MediaType.IMAGE)
call.response.status(HttpStatusCode.Created)
call.respond(
CheerzResponse(media, null)
)
}
else -> {
call.response.status(HttpStatusCode.BadRequest)
call.respond(
CheerzResponse(null, "Bad request")
)
}
}
part.dispose()
}

call.response.status(HttpStatusCode.BadRequest)
call.respond(
CheerzResponse(null, "Bad request")
)
call.respond(media)
}
}
}

private fun Route.download() {
get("/download/{id}") {
val id = call.parameters["id"]

val bucketName = call.application.environment.config.property("ktor.storage.bucket_name").getString()
val blob = storage.get(bucketName).get(id)

if (blob == null) {
call.response.status(HttpStatusCode.NotFound)
call.respond(
CheerzResponse(null, "Not found")
)
return@get
}

call.respondBytes(blob.getContent(), ContentType.Image.JPEG, HttpStatusCode.OK)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import com.cheerz.mediamanager.models.MediaType

val mediaStorage = listOf(
MediaItem(
"image1",
"image_1",
MediaType.IMAGE,
),
MediaItem(
"video1",
"video_1",
MediaType.VIDEO,
)
)
)
13 changes: 13 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ktor {
deployment {
port = 8080
port = ${?PORT}
}
application {
modules = [ com.cheerz.mediamanager.ApplicationKt.module ]
}
storage {
bucket_name = "cheerz_medias"
bucket_name = ${?BUCKET_NAME}
}
}
8 changes: 4 additions & 4 deletions src/test/kotlin/com/cheerz/ApplicationTest.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.cheerz

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication
import kotlin.test.Test
import kotlin.test.assertEquals

Expand Down