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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 📝 Description

## 🖼️ Media

## ✅ Checklist
* [ ] PR has been proofread by the author

https://cheerz0.atlassian.net/browse/
20 changes: 20 additions & 0 deletions .github/workflows/pr-title-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: "Lint PR Title"

on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize

jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: pragmatic-tools/pr-title-validator@1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
pattern: ^(\[(Feat|Fix|Tech|UI)\] ([A-Z\d]+)-\d+ - .+)|(Bump version \d{4}\.\d+\.\d+)|(Release \d{4}\.\d+\.\d+)$
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.


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 }
10 changes: 3 additions & 7 deletions src/main/kotlin/com/cheerz/mediamanager/plugins/Routing.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
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()

testSerializationRoute()
mediaRoutes()
}
}
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
12 changes: 0 additions & 12 deletions src/main/kotlin/com/cheerz/mediamanager/routes/Experimentations.kt

This file was deleted.

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!")
}
}
}
116 changes: 97 additions & 19 deletions src/main/kotlin/com/cheerz/mediamanager/routes/MediaRoutes.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,104 @@
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 io.ktor.server.routing.*

fun Route.listMediaRoute() {
get("/media") {
if (mediaStorage.isNotEmpty()) {
call.respond(mediaStorage)
}
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.Bucket
import com.google.cloud.storage.Storage
import io.ktor.http.ContentType
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.ApplicationCall
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.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.util.pipeline.PipelineContext


fun Route.mediaRoutes() = route("/media") {
listMedias(contentType = "image")
listMedias(contentType = "video")
upload()
download()
}

private fun Route.listMedias(contentType: String) {
get("/${contentType}s") {
val buckets = getBucket().list(Storage.BlobListOption.currentDirectory()).values
val medias = buckets
.filter { it.contentType?.startsWith(contentType) == 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()

getBucket().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 blob = getBucket().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)
}
}

private fun PipelineContext<*, ApplicationCall>.getBucket(): Bucket {
val bucketName = call.application.environment.config.property("ktor.storage.bucket_name").getString()
return storage.get(bucketName)
}
15 changes: 0 additions & 15 deletions src/main/kotlin/com/cheerz/mediamanager/storage/RamMediaStorage.kt

This file was deleted.

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
Loading