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 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)
} }

View file

@ -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) {

View file

@ -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")
} }
} }

View file

@ -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()

View file

@ -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)

View file

@ -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")
} }
} }

View file

@ -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) {