feat(Http): support response caching

This commit is contained in:
rushiiMachine 2025-04-09 23:31:14 -07:00
parent 38e300ef38
commit c840aac67e
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
7 changed files with 80 additions and 12 deletions

View file

@ -1,18 +1,30 @@
package com.aliucord.manager.di
import android.app.Application
import com.aliucord.manager.BuildConfig
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
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.defaultRequest
import io.ktor.client.request.HttpRequestBuilder
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.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 okhttp3.Dns
import org.koin.core.scope.Scope
import java.net.Inet4Address
import java.net.InetAddress
import kotlin.coroutines.CoroutineContext
@Suppress("UnusedReceiverParameter")
fun Scope.provideJson() = Json {
@ -21,10 +33,12 @@ fun Scope.provideJson() = Json {
fun Scope.provideHttpClient() = HttpClient(OkHttp) {
val json: Json = get()
val application: Application = get()
defaultRequest {
header(HttpHeaders.UserAgent, "Aliucord Manager/${BuildConfig.VERSION_NAME}")
}
engine {
config {
dns(object : Dns {
@ -42,7 +56,43 @@ fun Scope.provideHttpClient() = HttpClient(OkHttp) {
})
}
}
install(ContentNegotiation) {
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)
}

View file

@ -27,6 +27,8 @@ class KtorDownloadManager(private val http: HttpClient) : IDownloadManager {
try {
val httpStmt = http.prepareGet(url) {
header(HttpHeaders.CacheControl, "no-cache, no-store")
// Disable compression due to bug on emulators
// This header cannot be set with Android's DownloadManager
if (IS_PROBABLY_EMULATOR) {

View file

@ -2,17 +2,23 @@ package com.aliucord.manager.network.services
import com.aliucord.manager.network.models.BuildInfo
import com.aliucord.manager.network.utils.ApiResponse
import io.ktor.client.request.header
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import io.ktor.http.HttpHeaders
class AliucordGithubService(
val github: GithubService,
val http: HttpService,
) {
suspend fun getDataJson(): ApiResponse<BuildInfo> {
return withContext(Dispatchers.IO) {
http.request { url(DATA_JSON_URL) }
/**
* Fetches the build data of Aliucord (excluding Aliuhook).
* @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")
}
}

View file

@ -1,12 +1,22 @@
package com.aliucord.manager.network.services
import com.aliucord.manager.di.cacheControl
import com.aliucord.manager.network.utils.*
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.http.HttpStatusCode
import io.ktor.http.*
class AliucordMavenService(private val http: HttpService) {
suspend fun getAliuhookVersion(): ApiResponse<SemVer> {
val metadataResponse = http.request<String> { url(ALIUHOOK_METADATA_URL) }
suspend fun getAliuhookVersion(force: Boolean = false): ApiResponse<SemVer> {
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 {
val versionString = "<release>(.+?)</release>".toRegex()

View file

@ -35,7 +35,7 @@ class DownloadAliuhookStep : DownloadStep(), IDexProvider, KoinComponent {
override suspend fun execute(container: StepRunner) {
container.log("Obtaining latest aliuhook version")
targetVersion = maven.getAliuhookVersion().getOrThrow()
targetVersion = maven.getAliuhookVersion(force = true).getOrThrow()
container.log("Fetched aliuhook version: $targetVersion")
super.execute(container)

View file

@ -26,7 +26,7 @@ class FetchInfoStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) {
container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}")
data = github.getDataJson().getOrThrow()
data = github.getBuildData(force = true).getOrThrow()
container.log("Fetched build data: $data")
}
}

View file

@ -154,7 +154,7 @@ class HomeModel(
}
suspend fun fetchRemoteData() {
github.getDataJson().fold(
github.getBuildData().fold(
success = {
val versionCode = it.discordVersionCode.toIntOrNull()
if (versionCode == null) {