-
Notifications
You must be signed in to change notification settings - Fork 541
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This pulls in the GCS endpoint from #5398 as part of a broader effort of splitting it up.
- Loading branch information
1 parent
ea37cb6
commit e250f69
Showing
3 changed files
with
149 additions
and
0 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
scripts/src/java/org/oppia/android/scripts/gae/gcs/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
], | ||
) |
21 changes: 21 additions & 0 deletions
21
scripts/src/java/org/oppia/android/scripts/gae/gcs/GcsEndpointApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
105
scripts/src/java/org/oppia/android/scripts/gae/gcs/GcsService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |