Skip to content

Commit

Permalink
Introduce GCS Retrofit endpoint.
Browse files Browse the repository at this point in the history
This pulls in the GCS endpoint from #5398 as part of a broader effort of
splitting it up.
  • Loading branch information
BenHenning committed May 27, 2024
1 parent ea37cb6 commit e250f69
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
23 changes: 23 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/gae/gcs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Library for providing the endpoint functionality to inspect and download assets from Google Cloud
Storage.
"""

load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")

kt_jvm_library(
name = "api",
testonly = True,
srcs = [
"GcsEndpointApi.kt",
"GcsService.kt",
],
visibility = [
"//scripts/src/java/org/oppia/android/scripts/gae:__subpackages__",
],
deps = [
"//third_party:com_squareup_retrofit2_converter-moshi",
"//third_party:com_squareup_retrofit2_retrofit",
"//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.oppia.android.scripts.gae.gcs

import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
import retrofit2.http.Streaming

interface GcsEndpointApi {
@GET("{gcs_bucket}/{entity_type}/{entity_id}/assets/{image_type}/{image_filename}")
@Headers("Content-Type:application/octet-stream")
@Streaming
fun fetchImageData(
@Path("gcs_bucket") gcsBucket: String,
@Path("entity_type") entityType: String,
@Path("entity_id") entityId: String,
@Path("image_type") imageType: String,
@Path("image_filename") imageFilename: String
): Call<ResponseBody>
}
105 changes: 105 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/gae/gcs/GcsService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.oppia.android.scripts.gae.gcs

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import okhttp3.Request
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit

class GcsService(private val baseUrl: String, private val gcsBucket: String) {
private val retrofit by lazy { Retrofit.Builder().baseUrl(baseUrl).build() }
private val apiService by lazy { retrofit.create(GcsEndpointApi::class.java) }

fun fetchImageContentLengthAsync(
imageContainerType: ImageContainerType,
imageType: ImageType,
entityId: String,
imageFilename: String
): Deferred<Long?> {
return apiService.fetchImageData(
gcsBucket,
imageContainerType.httpRepresentation,
entityId,
imageType.httpRepresentation,
imageFilename
).resolveAsync(
transform = { request, response ->
checkNotNull(response.body()) {
"Failed to receive body for request: $request."
}.use { it.contentLength() }
},
default = { _, _, -> null }
// default = { request, response ->
// error("Failed to call: $request. Encountered failure:\n$response")
// }
)
}

fun fetchImageContentDataAsync(
imageContainerType: ImageContainerType,
imageType: ImageType,
entityId: String,
imageFilename: String
): Deferred<ByteArray?> {
return apiService.fetchImageData(
gcsBucket,
imageContainerType.httpRepresentation,
entityId,
imageType.httpRepresentation,
imageFilename
).resolveAsync(
transform = { request, response ->
checkNotNull(response.body()) { "Failed to receive body for request: $request." }.use {
it.byteStream().readBytes()
}
},
default = { _, _ -> null }
// default = { request, response ->
// error("Failed to call: $request. Encountered failure:\n$response")
// }
)
}

fun computeImageUrl(
imageContainerType: ImageContainerType,
imageType: ImageType,
entityId: String,
imageFilename: String
): String {
val containerTypeHttpRep = imageContainerType.httpRepresentation
val imgTypeHttpRep = imageType.httpRepresentation
return "${baseUrl.removeSuffix("/")}/$gcsBucket/$containerTypeHttpRep/$entityId/assets" +
"/$imgTypeHttpRep/$imageFilename"
}

enum class ImageContainerType(val httpRepresentation: String) {
EXPLORATION(httpRepresentation = "exploration"),
SKILL(httpRepresentation = "skill"),
TOPIC(httpRepresentation = "topic"),
STORY(httpRepresentation = "story")
}

enum class ImageType(val httpRepresentation: String) {
HTML_IMAGE(httpRepresentation = "image"),
THUMBNAIL(httpRepresentation = "thumbnail")
}

private companion object {
private fun <I, O> Call<I>.resolveAsync(
transform: (Request, Response<I>) -> O,
default: (Request, Response<I>) -> O
): Deferred<O> {
// Use the I/O dispatcher for blocking HTTP operations (since it's designed to handle blocking
// operations that might otherwise stall a coroutine dispatcher).
return CoroutineScope(Dispatchers.IO).async {
val result = execute()
return@async if (result.isSuccessful) {
transform(request(), result)
} else default(request(), result)
}
}
}
}

0 comments on commit e250f69

Please sign in to comment.