feat(Http): support response caching
This commit is contained in:
parent
38e300ef38
commit
c840aac67e
7 changed files with 80 additions and 12 deletions
|
|
@ -1,18 +1,30 @@
|
||||||
package com.aliucord.manager.di
|
package com.aliucord.manager.di
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import com.aliucord.manager.BuildConfig
|
import com.aliucord.manager.BuildConfig
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.HttpClientCall
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
|
import io.ktor.client.plugins.cache.HttpCache
|
||||||
|
import io.ktor.client.plugins.cache.storage.FileStorage
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.plugins.defaultRequest
|
import io.ktor.client.plugins.defaultRequest
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.client.statement.HttpReceivePipeline
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.util.AttributeKey
|
||||||
|
import io.ktor.util.date.GMTDate
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.InternalAPI
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
@Suppress("UnusedReceiverParameter")
|
||||||
fun Scope.provideJson() = Json {
|
fun Scope.provideJson() = Json {
|
||||||
|
|
@ -21,10 +33,12 @@ fun Scope.provideJson() = Json {
|
||||||
|
|
||||||
fun Scope.provideHttpClient() = HttpClient(OkHttp) {
|
fun Scope.provideHttpClient() = HttpClient(OkHttp) {
|
||||||
val json: Json = get()
|
val json: Json = get()
|
||||||
|
val application: Application = get()
|
||||||
|
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
header(HttpHeaders.UserAgent, "Aliucord Manager/${BuildConfig.VERSION_NAME}")
|
header(HttpHeaders.UserAgent, "Aliucord Manager/${BuildConfig.VERSION_NAME}")
|
||||||
}
|
}
|
||||||
|
|
||||||
engine {
|
engine {
|
||||||
config {
|
config {
|
||||||
dns(object : Dns {
|
dns(object : Dns {
|
||||||
|
|
@ -42,7 +56,43 @@ fun Scope.provideHttpClient() = HttpClient(OkHttp) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(json)
|
json(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install(HttpCache) {
|
||||||
|
val dir = application.cacheDir.resolve("ktor")
|
||||||
|
publicStorage(FileStorage(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom plugin to allow overriding response cache headers, and force caching
|
||||||
|
install("OverrideCacheControl") {
|
||||||
|
receivePipeline.intercept(HttpReceivePipeline.Before) { response ->
|
||||||
|
val customCacheControl = response.call.attributes.getOrNull(CustomCacheControl)
|
||||||
|
?: return@intercept
|
||||||
|
|
||||||
|
proceedWith(object : HttpResponse() {
|
||||||
|
@InternalAPI
|
||||||
|
override val rawContent: ByteReadChannel = response.rawContent
|
||||||
|
override val call: HttpClientCall = response.call
|
||||||
|
override val coroutineContext: CoroutineContext = response.coroutineContext
|
||||||
|
override val requestTime: GMTDate = response.requestTime
|
||||||
|
override val responseTime: GMTDate = response.responseTime
|
||||||
|
override val status: HttpStatusCode = response.status
|
||||||
|
override val version: HttpProtocolVersion = response.version
|
||||||
|
|
||||||
|
override val headers: Headers = headers {
|
||||||
|
appendAll(response.headers)
|
||||||
|
set(HttpHeaders.CacheControl, customCacheControl.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CustomCacheControl: AttributeKey<CacheControl> = AttributeKey("CustomCacheControl")
|
||||||
|
|
||||||
|
fun HttpRequestBuilder.cacheControl(cacheControl: CacheControl) {
|
||||||
|
attributes.put(CustomCacheControl, cacheControl)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ class KtorDownloadManager(private val http: HttpClient) : IDownloadManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val httpStmt = http.prepareGet(url) {
|
val httpStmt = http.prepareGet(url) {
|
||||||
|
header(HttpHeaders.CacheControl, "no-cache, no-store")
|
||||||
|
|
||||||
// Disable compression due to bug on emulators
|
// Disable compression due to bug on emulators
|
||||||
// This header cannot be set with Android's DownloadManager
|
// This header cannot be set with Android's DownloadManager
|
||||||
if (IS_PROBABLY_EMULATOR) {
|
if (IS_PROBABLY_EMULATOR) {
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,23 @@ package com.aliucord.manager.network.services
|
||||||
|
|
||||||
import com.aliucord.manager.network.models.BuildInfo
|
import com.aliucord.manager.network.models.BuildInfo
|
||||||
import com.aliucord.manager.network.utils.ApiResponse
|
import com.aliucord.manager.network.utils.ApiResponse
|
||||||
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import kotlinx.coroutines.Dispatchers
|
import io.ktor.http.HttpHeaders
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class AliucordGithubService(
|
class AliucordGithubService(
|
||||||
val github: GithubService,
|
val github: GithubService,
|
||||||
val http: HttpService,
|
val http: HttpService,
|
||||||
) {
|
) {
|
||||||
suspend fun getDataJson(): ApiResponse<BuildInfo> {
|
/**
|
||||||
return withContext(Dispatchers.IO) {
|
* Fetches the build data of Aliucord (excluding Aliuhook).
|
||||||
http.request { url(DATA_JSON_URL) }
|
* @param force Whether to force Ktor to refetch/revalidate cache.
|
||||||
|
*/
|
||||||
|
suspend fun getBuildData(force: Boolean = false): ApiResponse<BuildInfo> = http.request {
|
||||||
|
url(DATA_JSON_URL)
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
header(HttpHeaders.CacheControl, "no-cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
package com.aliucord.manager.network.services
|
package com.aliucord.manager.network.services
|
||||||
|
|
||||||
|
import com.aliucord.manager.di.cacheControl
|
||||||
import com.aliucord.manager.network.utils.*
|
import com.aliucord.manager.network.utils.*
|
||||||
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.*
|
||||||
|
|
||||||
class AliucordMavenService(private val http: HttpService) {
|
class AliucordMavenService(private val http: HttpService) {
|
||||||
suspend fun getAliuhookVersion(): ApiResponse<SemVer> {
|
suspend fun getAliuhookVersion(force: Boolean = false): ApiResponse<SemVer> {
|
||||||
val metadataResponse = http.request<String> { url(ALIUHOOK_METADATA_URL) }
|
val metadataResponse = http.request<String> {
|
||||||
|
url(ALIUHOOK_METADATA_URL)
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
cacheControl(CacheControl.MaxAge(maxAgeSeconds = 60 * 30)) // 30 min
|
||||||
|
} else {
|
||||||
|
header(HttpHeaders.CacheControl, "no-cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return metadataResponse.transform {
|
return metadataResponse.transform {
|
||||||
val versionString = "<release>(.+?)</release>".toRegex()
|
val versionString = "<release>(.+?)</release>".toRegex()
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class DownloadAliuhookStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
container.log("Obtaining latest aliuhook version")
|
container.log("Obtaining latest aliuhook version")
|
||||||
targetVersion = maven.getAliuhookVersion().getOrThrow()
|
targetVersion = maven.getAliuhookVersion(force = true).getOrThrow()
|
||||||
container.log("Fetched aliuhook version: $targetVersion")
|
container.log("Fetched aliuhook version: $targetVersion")
|
||||||
|
|
||||||
super.execute(container)
|
super.execute(container)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class FetchInfoStep : Step(), KoinComponent {
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}")
|
container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}")
|
||||||
data = github.getDataJson().getOrThrow()
|
data = github.getBuildData(force = true).getOrThrow()
|
||||||
container.log("Fetched build data: $data")
|
container.log("Fetched build data: $data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ class HomeModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchRemoteData() {
|
suspend fun fetchRemoteData() {
|
||||||
github.getDataJson().fold(
|
github.getBuildData().fold(
|
||||||
success = {
|
success = {
|
||||||
val versionCode = it.discordVersionCode.toIntOrNull()
|
val versionCode = it.discordVersionCode.toIntOrNull()
|
||||||
if (versionCode == null) {
|
if (versionCode == null) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue