Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,65 @@ package zed.rainxch.core.data.mirror
import zed.rainxch.core.domain.model.MirrorConfig
import zed.rainxch.core.domain.model.MirrorStatus
import zed.rainxch.core.domain.model.MirrorType
import zed.rainxch.core.domain.model.TrafficKind

object BundledMirrors {
private val FULL_PROXY_KINDS = setOf(TrafficKind.RELEASE_ASSET, TrafficKind.RAW_FILE)
private val RAW_FILE_ONLY = setOf(TrafficKind.RAW_FILE)

val ALL: List<MirrorConfig> =
listOf(
entry("direct", "Direct GitHub", null, MirrorType.OFFICIAL),
entry("ghfast_top", "ghfast.top", "https://ghfast.top/{url}", MirrorType.COMMUNITY),
entry("moeyy_xyz", "github.moeyy.xyz", "https://github.moeyy.xyz/{url}", MirrorType.COMMUNITY),
entry("gh_proxy_com", "gh-proxy.com", "https://gh-proxy.com/{url}", MirrorType.COMMUNITY),
entry("ghps_cc", "ghps.cc", "https://ghps.cc/{url}", MirrorType.COMMUNITY),
entry("gh_99988866_xyz", "gh.api.99988866.xyz", "https://gh.api.99988866.xyz/{url}", MirrorType.COMMUNITY),
entry(
id = "direct",
name = "Direct GitHub",
template = null,
type = MirrorType.OFFICIAL
),
entry(
id = "fastly_jsdelivr",
name = "fastly.jsdelivr.net",
template = "https://fastly.jsdelivr.net/gh/{owner}/{repo}@{ref}/{path}",
type = MirrorType.COMMUNITY,
trafficKinds = RAW_FILE_ONLY,
),
entry(
id = "ghfast_top",
name = "ghfast.top",
template = "https://ghfast.top/{url}",
type = MirrorType.COMMUNITY
),
entry(
id = "gh_proxy_com",
name = "gh-proxy.com",
template = "https://gh-proxy.com/{url}",
type = MirrorType.COMMUNITY
),
entry(
id = "moeyy_xyz",
name = "github.moeyy.xyz",
template = "https://github.moeyy.xyz/{url}",
type = MirrorType.COMMUNITY
),
entry(
id = "ghps_cc",
name = "ghps.cc",
template = "https://ghps.cc/{url}",
type = MirrorType.COMMUNITY
),
entry(
id = "gh_99988866_xyz",
name = "gh.api.99988866.xyz",
template = "https://gh.api.99988866.xyz/{url}",
type = MirrorType.COMMUNITY,
),
)

private fun entry(
id: String,
name: String,
template: String?,
type: MirrorType,
trafficKinds: Set<TrafficKind> = FULL_PROXY_KINDS,
) = MirrorConfig(
id = id,
name = name,
Expand All @@ -28,5 +70,6 @@ object BundledMirrors {
status = MirrorStatus.UNKNOWN,
latencyMs = null,
lastCheckedAt = null,
trafficKinds = trafficKinds,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class MirrorRepositoryImpl(
private val ksafe: KSafe,
private val legacyDataStore: DataStore<Preferences>,
private val apiClient: MirrorApiClient,
private val appScope: CoroutineScope,
appScope: CoroutineScope,
) : MirrorRepository {
private val json = Json { ignoreUnknownKeys = true }
private val cacheTtlMs = 60L * 60 * 1000
Expand All @@ -64,12 +64,30 @@ class MirrorRepositoryImpl(
ksafe = ksafe,
markerKey = MIGRATION_MARKER,
entries = listOf(
MigrationEntry(stringPreferencesKey("mirror_preferred_id"), K_PREFERRED),
MigrationEntry(stringPreferencesKey("mirror_custom_template"), K_CUSTOM_TEMPLATE),
MigrationEntry(stringPreferencesKey("mirror_cached_list_json"), K_CACHED_JSON),
MigrationEntry(longPreferencesKey("mirror_cached_list_at"), K_CACHED_AT),
MigrationEntry(longPreferencesKey("mirror_auto_suggest_snooze_until"), K_SUGGEST_SNOOZE),
MigrationEntry(booleanPreferencesKey("mirror_auto_suggest_dismissed"), K_SUGGEST_DISMISSED),
MigrationEntry(
legacyKey = stringPreferencesKey("mirror_preferred_id"),
ksafeKey = K_PREFERRED
),
MigrationEntry(
legacyKey = stringPreferencesKey("mirror_custom_template"),
ksafeKey = K_CUSTOM_TEMPLATE
),
MigrationEntry(
legacyKey = stringPreferencesKey("mirror_cached_list_json"),
ksafeKey = K_CACHED_JSON
),
MigrationEntry(
legacyKey = longPreferencesKey("mirror_cached_list_at"),
ksafeKey = K_CACHED_AT
),
MigrationEntry(
legacyKey = longPreferencesKey("mirror_auto_suggest_snooze_until"),
ksafeKey = K_SUGGEST_SNOOZE
),
MigrationEntry(
legacyKey = booleanPreferencesKey("mirror_auto_suggest_dismissed"),
ksafeKey = K_SUGGEST_DISMISSED
),
),
)
}
Expand All @@ -91,7 +109,10 @@ class MirrorRepositoryImpl(
val configs = response.mirrors.map { it.toDomain() }
val previousCatalog = _catalog.value
_catalog.value = configs
ksafe.safePut(K_CACHED_JSON, json.encodeToString(MirrorListResponse.serializer(), response))
ksafe.safePut(
K_CACHED_JSON,
json.encodeToString(MirrorListResponse.serializer(), response)
)
ksafe.safePut(K_CACHED_AT, Clock.System.now().toEpochMilliseconds())
checkSelectedMirrorStillExists(fresh = configs, previous = previousCatalog)
}.map { }
Expand All @@ -106,7 +127,10 @@ class MirrorRepositoryImpl(
when (id) {
DIRECT_MIRROR_ID -> MirrorPreference.Direct
CUSTOM_MIRROR_ID_SENTINEL ->
if (template.isBlank()) MirrorPreference.Direct else MirrorPreference.Custom(template)
if (template.isBlank()) MirrorPreference.Direct else MirrorPreference.Custom(
template
)

else -> MirrorPreference.Selected(id)
}
},
Expand All @@ -120,10 +144,12 @@ class MirrorRepositoryImpl(
ksafe.safePut(K_PREFERRED, DIRECT_MIRROR_ID)
ksafe.safeDelete(K_CUSTOM_TEMPLATE)
}

is MirrorPreference.Selected -> {
ksafe.safePut(K_PREFERRED, pref.id)
ksafe.safeDelete(K_CUSTOM_TEMPLATE)
}

is MirrorPreference.Custom -> {
ksafe.safePut(K_PREFERRED, CUSTOM_MIRROR_ID_SENTINEL)
ksafe.safePut(K_CUSTOM_TEMPLATE, pref.template)
Expand All @@ -144,13 +170,21 @@ class MirrorRepositoryImpl(
}

private suspend fun readCachedCatalogOrBundled(): List<MirrorConfig> {
val cachedJson = runCatching { ksafe.safeGet(K_CACHED_JSON, "") }.getOrDefault("")
val cachedJson = runCatching {
ksafe.safeGet(key = K_CACHED_JSON, defaultValue = "")
}.getOrDefault("")

return if (cachedJson.isBlank()) {
BundledMirrors.ALL
BundledMirrors.ALL.sortedBy { it.status.ordinal }
} else {
runCatching {
json.decodeFromString(MirrorListResponse.serializer(), cachedJson).mirrors.map { it.toDomain() }
}.getOrElse { BundledMirrors.ALL }
json.decodeFromString(
deserializer = MirrorListResponse.serializer(),
string = cachedJson
).mirrors
.map { it.toDomain() }
.sortedBy { it.status.ordinal }
}.getOrElse { BundledMirrors.ALL.sortedBy { it.status.ordinal } }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import zed.rainxch.githubstore.core.presentation.res.Res
import zed.rainxch.githubstore.core.presentation.res.error_unknown
import zed.rainxch.githubstore.core.presentation.res.mirror_custom_validation_https
import zed.rainxch.githubstore.core.presentation.res.mirror_custom_validation_template
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource

class MirrorPickerViewModel(
Expand Down Expand Up @@ -53,14 +54,22 @@ class MirrorPickerViewModel(

fun onAction(action: MirrorPickerAction) {
when (action) {
MirrorPickerAction.OnNavigateBack -> { }
MirrorPickerAction.OnNavigateBack -> {}
is MirrorPickerAction.OnSelectMirror -> selectMirror(action.mirror)
MirrorPickerAction.OnCustomMirrorClicked ->
_state.update { it.copy(isCustomDialogVisible = true, customDraft = "", customDraftError = null) }
_state.update {
it.copy(
isCustomDialogVisible = true,
customDraft = "",
customDraftError = null
)
}

is MirrorPickerAction.OnCustomDraftChanged -> updateDraft(action.value)
MirrorPickerAction.OnCustomMirrorConfirm -> confirmCustom()
MirrorPickerAction.OnCustomMirrorDismiss ->
_state.update { it.copy(isCustomDialogVisible = false) }

MirrorPickerAction.OnTestConnection -> runTest()
MirrorPickerAction.OnRefreshCatalog -> refresh()
MirrorPickerAction.OnDeployYourOwnClicked ->
Expand All @@ -72,8 +81,9 @@ class MirrorPickerViewModel(

private fun selectMirror(mirror: MirrorConfig) {
viewModelScope.launch {
val pref =
if (mirror.id == "direct") MirrorPreference.Direct else MirrorPreference.Selected(mirror.id)
val pref = if (mirror.id == "direct") {
MirrorPreference.Direct
} else MirrorPreference.Selected(mirror.id)
mirrorRepository.setPreference(pref)
}
}
Expand All @@ -95,44 +105,67 @@ class MirrorPickerViewModel(
if (draft.isBlank() || error != null) return
viewModelScope.launch {
mirrorRepository.setPreference(MirrorPreference.Custom(draft))
_state.update { it.copy(isCustomDialogVisible = false, customDraft = "", customDraftError = null) }
_state.update {
it.copy(
isCustomDialogVisible = false,
customDraft = "",
customDraftError = null
)
}
}
}

private fun runTest() {
viewModelScope.launch {
_state.update { it.copy(isTesting = true, testResult = null) }
val pref = state.value.preference
val template =
when (pref) {
when (val pref = state.value.preference) {
MirrorPreference.Direct -> null
is MirrorPreference.Custom -> pref.template
is MirrorPreference.Selected ->
state.value.mirrors.firstOrNull { it.id == pref.id }?.urlTemplate
}
val probeUrl = "https://raw.githubusercontent.com/octocat/Hello-World/master/README"
val targetUrl =
if (template == null) probeUrl
else template.replace("{url}", probeUrl)
val result =
withTimeoutOrNull(5_000L) {
runCatching {
val mark = TimeSource.Monotonic.markNow()
val response = testHttpClient.get(targetUrl)
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
response.status.value to elapsedMs
}
val wholeUrlProbe =
"https://raw.githubusercontent.com/octocat/Hello-World/master/README"

val targetUrl = when {
template == null -> wholeUrlProbe
template.contains("{url}") -> template.replace("{url}", wholeUrlProbe)
template.contains("{owner}") -> template
.replace("{owner}", "cli")
.replace("{repo}", "cli")
.replace("{ref}", "v2.40.0")
.replace("{path}", "LICENSE")

else -> wholeUrlProbe
}

val result = withTimeoutOrNull(5_000L.milliseconds) {
runCatching {
val mark = TimeSource.Monotonic.markNow()
val response = testHttpClient.get(targetUrl)
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
response.status.value to elapsedMs
}
val testResult: TestResult =
when {
result == null -> TestResult.Timeout
result.isSuccess -> {
val (status, ms) = result.getOrThrow()
if (status in 200..299) TestResult.Success(ms) else TestResult.HttpError(status)
}
result.exceptionOrNull() is UnresolvedAddressException -> TestResult.DnsFailure
else -> TestResult.Other(result.exceptionOrNull()?.message ?: getString(Res.string.error_unknown))
}

val testResult: TestResult = when {
result == null -> TestResult.Timeout

result.isSuccess -> {
val (status, ms) = result.getOrThrow()
if (status in 200..299) {
TestResult.Success(ms)
} else TestResult.HttpError(status)
}

result.exceptionOrNull() is UnresolvedAddressException -> TestResult.DnsFailure

else -> TestResult.Other(
result.exceptionOrNull()?.message ?: getString(Res.string.error_unknown)
)
}

_state.update { it.copy(isTesting = false, testResult = testResult) }
}
}
Expand Down