Skip to content

Commit 188221a

Browse files
committed
feat: refactor GeoIP DB update handling and improve error management
1 parent 9c30e78 commit 188221a

File tree

10 files changed

+76
-44
lines changed

10 files changed

+76
-44
lines changed

composeApp/src/androidMain/kotlin/org/ooni/probe/net/Http.android.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package org.ooni.probe.net
22

33
import kotlinx.coroutines.Dispatchers
44
import kotlinx.coroutines.withContext
5+
import org.ooni.engine.models.Failure
6+
import org.ooni.engine.models.Result
7+
import org.ooni.engine.models.Success
8+
import org.ooni.probe.data.models.GetBytesException
59
import java.net.HttpURLConnection
610
import java.net.URL
711

8-
actual suspend fun httpGetBytes(url: String): ByteArray =
12+
actual suspend fun httpGetBytes(url: String): Result<ByteArray, GetBytesException> =
913
withContext(Dispatchers.IO) {
1014
val connection = (URL(url).openConnection() as HttpURLConnection)
1115
connection.requestMethod = "GET"
@@ -16,8 +20,13 @@ actual suspend fun httpGetBytes(url: String): ByteArray =
1620
val code = connection.responseCode
1721
val stream = if (code in 200..299) connection.inputStream else connection.errorStream
1822
val bytes = stream?.use { it.readBytes() } ?: ByteArray(0)
19-
if (code !in 200..299) throw RuntimeException("HTTP $code while GET $url: ${String(bytes)}")
20-
bytes
23+
if (code !in 200..299) {
24+
Failure(GetBytesException(RuntimeException("HTTP $code while GET $url: ${String(bytes)}")))
25+
} else {
26+
Success(bytes)
27+
}
28+
} catch (e: Throwable) {
29+
Failure(GetBytesException(e))
2130
} finally {
2231
connection.disconnect()
2332
}

composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ fun App(
120120
// dependencies.startSingleRunInner(RunSpecification.OnlyUploadMissingResults)
121121
}
122122
LaunchedEffect(Unit) {
123-
// Check for GeoIP DB updates in the background
124-
runCatching { dependencies.fetchGeoIpDbUpdates() }
123+
dependencies.fetchGeoIpDbUpdates()
125124
}
126125
LaunchedEffect(Unit) {
127126
dependencies.observeAndConfigureAutoUpdate()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.ooni.probe.data.models
2+
3+
class GetBytesException(
4+
t: Throwable,
5+
) : Exception(t)

composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
66
import androidx.datastore.preferences.core.booleanPreferencesKey
77
import androidx.datastore.preferences.core.edit
88
import androidx.datastore.preferences.core.intPreferencesKey
9+
import androidx.datastore.preferences.core.longPreferencesKey
910
import androidx.datastore.preferences.core.stringPreferencesKey
1011
import androidx.datastore.preferences.core.stringSetPreferencesKey
1112
import kotlinx.coroutines.flow.Flow
@@ -75,12 +76,14 @@ class PreferenceRepository(
7576
): PreferenceKey<*> {
7677
val preferenceKey = getPreferenceKey(name = key.value, prefix = prefix, autoRun = autoRun)
7778
return when (key) {
78-
SettingsKey.MMDB_LAST_CHECK,
7979
SettingsKey.MAX_RUNTIME,
8080
SettingsKey.LEGACY_PROXY_PORT,
8181
SettingsKey.DELETE_OLD_RESULTS_THRESHOLD,
8282
-> PreferenceKey.IntKey(intPreferencesKey(preferenceKey))
8383

84+
SettingsKey.MMDB_LAST_CHECK,
85+
-> PreferenceKey.LongKey(longPreferencesKey(preferenceKey))
86+
8487
SettingsKey.MMDB_VERSION,
8588
SettingsKey.LEGACY_PROXY_HOSTNAME,
8689
SettingsKey.LEGACY_PROXY_PROTOCOL,

composeApp/src/commonMain/kotlin/org/ooni/probe/domain/DownloadFile.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import okio.Path
55
import okio.Path.Companion.toPath
66
import okio.buffer
77
import okio.use
8+
import org.ooni.engine.models.Result
9+
import org.ooni.probe.data.models.GetBytesException
810

911
/**
1012
* Downloads binary content to a target absolute path using the provided fetcher.
@@ -13,20 +15,19 @@ import okio.use
1315
*/
1416
class DownloadFile(
1517
private val fileSystem: FileSystem,
16-
private val fetchBytes: suspend (url: String) -> ByteArray,
18+
private val fetchBytes: suspend (url: String) -> Result<ByteArray, GetBytesException>,
1719
) {
1820
suspend operator fun invoke(
1921
url: String,
2022
absoluteTargetPath: String,
21-
): Path {
23+
): Result<Path, GetBytesException> {
2224
val target = absoluteTargetPath.toPath()
2325
target.parent?.let { parent ->
2426
if (fileSystem.metadataOrNull(parent) == null) fileSystem.createDirectories(parent)
2527
}
26-
val bytes = fetchBytes(url)
27-
val existing = fileSystem.metadataOrNull(target)
28-
if (existing?.size == bytes.size.toLong()) return target
29-
fileSystem.sink(target).buffer().use { sink -> sink.write(bytes) }
30-
return target
28+
return fetchBytes(url).map { bytes ->
29+
fileSystem.sink(target).buffer().use { sink -> sink.write(bytes) }
30+
target
31+
}
3132
}
3233
}

composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchGeoIpDbUpdates.kt

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,41 @@ import org.ooni.engine.models.Failure
1313
import org.ooni.engine.models.Result
1414
import org.ooni.engine.models.Success
1515
import org.ooni.engine.models.TaskOrigin
16+
import org.ooni.probe.data.models.GetBytesException
1617
import org.ooni.probe.data.models.SettingsKey
1718
import org.ooni.probe.data.repositories.PreferenceRepository
1819
import kotlin.time.Clock
1920

2021
class FetchGeoIpDbUpdates(
21-
private val downloadFile: suspend (url: String, absoluteTargetPath: String) -> Path,
22+
private val downloadFile: suspend (url: String, absoluteTargetPath: String) -> Result<Path, GetBytesException>,
2223
private val cacheDir: String,
2324
private val engineHttpDo: suspend (method: String, url: String, taskOrigin: TaskOrigin) -> Result<String?, Engine.MkException>,
2425
private val preferencesRepository: PreferenceRepository,
2526
private val json: Json,
2627
) {
2728
suspend operator fun invoke(): Result<Path?, Engine.MkException> =
28-
try {
29-
when (val versionRes = getLatestEngineVersion()) {
30-
is Failure -> Failure(versionRes.reason)
31-
is Success -> {
32-
val (isLatest, _, latestVersion) = isGeoIpDbLatest(versionRes.value)
33-
if (isLatest) {
34-
Success(null)
35-
} else {
36-
val versionName = latestVersion
37-
val url = buildGeoIpDbUrl(versionName)
38-
val target = "$cacheDir/$versionName.mmdb"
29+
getLatestEngineVersion()
30+
.onSuccess { version ->
31+
val (isLatest, _, latestVersion) = isGeoIpDbLatest(version)
32+
if (isLatest) {
33+
return Success(null)
34+
} else {
35+
val versionName = latestVersion
36+
val url = buildGeoIpDbUrl(versionName)
37+
val target = "$cacheDir/$versionName.mmdb"
3938

40-
downloadFile(url, target).let {
39+
downloadFile(url, target)
40+
.onSuccess { downloadedPath ->
4141
preferencesRepository.setValueByKey(SettingsKey.MMDB_VERSION, versionName)
4242
preferencesRepository.setValueByKey(SettingsKey.MMDB_LAST_CHECK, Clock.System.now().toEpochMilliseconds())
43-
Success(it)
43+
return Success(downloadedPath)
44+
}.onFailure { downloadError ->
45+
return Failure(Engine.MkException(downloadError))
4446
}
45-
}
4647
}
47-
}
48-
} catch (t: Throwable) {
49-
Failure(t as? Engine.MkException ?: Engine.MkException(t))
50-
}
48+
}.onFailure { versionError ->
49+
return Failure(versionError)
50+
}.let { Failure(Engine.MkException(Throwable("Unexpected state"))) }
5151

5252
/**
5353
* Compare latest and current version integers and return pair of latest state and actual version number
@@ -62,7 +62,7 @@ class FetchGeoIpDbUpdates(
6262
}
6363

6464
private suspend fun getLatestEngineVersion(): Result<String, Engine.MkException> {
65-
val url = "https://api.0.github.com/repos/aanorbel/oomplt-mmdb/releases/latest"
65+
val url = "https://api.github.com/repos/aanorbel/oomplt-mmdb/releases/latest"
6666

6767
return engineHttpDo("GET", url, TaskOrigin.OoniRun).map { payload ->
6868
val jsonStr = payload ?: throw Engine.MkException(Throwable("Empty body"))
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package org.ooni.probe.net
22

3+
import org.ooni.engine.models.Result
4+
import org.ooni.probe.data.models.GetBytesException
5+
36
/**
47
* Perform a simple HTTP GET and return the raw response body bytes.
58
* Implemented per-platform to ensure binary-safe downloads.
69
*/
7-
expect suspend fun httpGetBytes(url: String): ByteArray
10+
expect suspend fun httpGetBytes(url: String): Result<ByteArray, GetBytesException>

composeApp/src/desktopMain/kotlin/org/ooni/engine/DesktopOonimkallBridge.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class DesktopOonimkallBridge : OonimkallBridge {
6666
it.assetsDir = assetsDir
6767
// geoipDB may or may not exist in this binding; set via reflection when available
6868
geoIpDB?.let { path ->
69-
// it.geoipDB = path
69+
it.geoipDB = path
7070
}
7171
it.stateDir = stateDir
7272
it.tempDir = tempDir

composeApp/src/desktopMain/kotlin/org/ooni/probe/net/Http.desktop.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package org.ooni.probe.net
22

33
import kotlinx.coroutines.Dispatchers
44
import kotlinx.coroutines.withContext
5+
import org.ooni.engine.models.Failure
6+
import org.ooni.engine.models.Result
7+
import org.ooni.engine.models.Success
8+
import org.ooni.probe.data.models.GetBytesException
59
import java.io.BufferedInputStream
610
import java.net.HttpURLConnection
711
import java.net.URL
812

9-
actual suspend fun httpGetBytes(url: String): ByteArray =
13+
actual suspend fun httpGetBytes(url: String): Result<ByteArray, GetBytesException> =
1014
withContext(Dispatchers.IO) {
1115
val connection = (URL(url).openConnection() as HttpURLConnection)
1216
connection.requestMethod = "GET"
@@ -17,8 +21,13 @@ actual suspend fun httpGetBytes(url: String): ByteArray =
1721
val code = connection.responseCode
1822
val stream = if (code in 200..299) connection.inputStream else connection.errorStream
1923
val bytes = stream?.let { BufferedInputStream(it).use { bis -> bis.readBytes() } } ?: ByteArray(0)
20-
if (code !in 200..299) throw RuntimeException("HTTP $code while GET $url: ${bytes.decodeToString()}")
21-
bytes
24+
if (code !in 200..299) {
25+
Failure(GetBytesException(RuntimeException("HTTP $code while GET $url: ${bytes.decodeToString()}")))
26+
} else {
27+
Success(bytes)
28+
}
29+
} catch (e: Throwable) {
30+
Failure(GetBytesException(e))
2231
} finally {
2332
connection.disconnect()
2433
}

composeApp/src/iosMain/kotlin/org/ooni/probe/net/Http.ios.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,34 @@ package org.ooni.probe.net
33
import kotlinx.coroutines.suspendCancellableCoroutine
44
import kotlinx.cinterop.addressOf
55
import kotlinx.cinterop.usePinned
6+
import org.ooni.engine.models.Failure
7+
import org.ooni.engine.models.Result
8+
import org.ooni.engine.models.Success
9+
import org.ooni.probe.data.models.GetBytesException
610
import platform.Foundation.NSData
711
import platform.Foundation.NSURL
812
import platform.Foundation.NSURLSession
913
import platform.Foundation.dataTaskWithURL
1014
import platform.posix.memcpy
1115
import kotlin.coroutines.resume
12-
import kotlin.coroutines.resumeWithException
1316

14-
actual suspend fun httpGetBytes(url: String): ByteArray =
17+
actual suspend fun httpGetBytes(url: String): Result<ByteArray, GetBytesException> =
1518
suspendCancellableCoroutine { cont ->
1619
val nsurl = NSURL.URLWithString(url)!!
1720
val task = NSURLSession.sharedSession.dataTaskWithURL(nsurl) { data, response, error ->
1821
when {
19-
error != null -> cont.resumeWithException(RuntimeException(error.localizedDescription))
22+
error != null -> cont.resume(Failure(GetBytesException(RuntimeException(error.localizedDescription))))
2023
data != null -> {
2124
// If we have an HTTP response, check status code
2225
val http = response as? platform.Foundation.NSHTTPURLResponse
2326
val status = http?.statusCode?.toInt() ?: 200
2427
if (status in 200..299) {
25-
cont.resume((data as NSData).toByteArray())
28+
cont.resume(Success((data as NSData).toByteArray()))
2629
} else {
27-
cont.resumeWithException(RuntimeException("HTTP $status while GET $url"))
30+
cont.resume(Failure(GetBytesException(RuntimeException("HTTP $status while GET $url"))))
2831
}
2932
}
30-
else -> cont.resume(ByteArray(0))
33+
else -> cont.resume(Success(ByteArray(0)))
3134
}
3235
}
3336
cont.invokeOnCancellation { task.cancel() }

0 commit comments

Comments
 (0)