chore: improve geo

This commit is contained in:
hstyi
2025-06-28 11:13:26 +08:00
committed by GitHub
parent 54116a4bf5
commit fff2dd89c7
8 changed files with 22 additions and 187 deletions

View File

@@ -2,12 +2,14 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
project.version = "0.0.2" project.version = "0.0.3"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
compileOnly(project(":")) compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1") implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202506280303")
} }
apply(from = "$rootDir/plugins/common.gradle.kts") apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -1,14 +1,12 @@
package app.termora.plugins.geo package app.termora.plugins.geo
import app.termora.Application
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Disposable import app.termora.Disposable
import app.termora.geo.GeoLibrary
import com.maxmind.db.CHMCache import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader import com.maxmind.geoip2.DatabaseReader
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -29,28 +27,20 @@ internal class Geo private constructor() : Disposable {
private var reader: DatabaseReader? = null private var reader: DatabaseReader? = null
private fun initialize() { private fun initialize() {
if (GeoApplicationRunnerExtension.instance.isReady().not()) return
if (isInitialized()) return if (isInitialized()) return
if (initialized.compareAndSet(false, true)) { if (initialized.compareAndSet(false, true)) {
try { try {
val database = getDatabaseFile() val input = GeoLibrary.getInputStream()
if ((database.exists() && database.isFile).not()) { if (input == null) {
throw IllegalStateException("${database.absolutePath} not be found") throw IllegalStateException("GeoLite2-Country.mmdb not be found")
} }
val locale = Locale.getDefault().toString().replace("_", "-") val locale = Locale.getDefault().toString().replace("_", "-")
try { try {
reader = DatabaseReader.Builder(database) reader = DatabaseReader.Builder(input)
.locales(listOf(locale, "en")) .locales(listOf(locale, "en"))
.withCache(CHMCache()).build() .withCache(CHMCache()).build()
} catch (e: Exception) { } catch (e: Exception) {
// 打开数据失败一般都是数据文件顺坏,删除数据库
FileUtils.deleteQuietly(database)
// 重新下载
GeoApplicationRunnerExtension.instance.reload()
throw e throw e
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -63,11 +53,6 @@ internal class Geo private constructor() : Disposable {
} }
fun getDatabaseFile(): File {
val dir = FileUtils.getFile(Application.getBaseDataDir(), "config", "plugins", "geo")
return File(dir, "GeoLite2-Country.mmdb")
}
fun country(ip: String): Country? { fun country(ip: String): Country? {
try { try {
initialize() initialize()

View File

@@ -1,108 +0,0 @@
package app.termora.plugins.geo
import app.termora.Application
import app.termora.ApplicationRunnerExtension
import app.termora.randomUUID
import app.termora.swingCoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.net.ProxySelector
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
class GeoApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(GeoApplicationRunnerExtension::class.java)
val instance = GeoApplicationRunnerExtension()
}
private var ready = false
private val httpClient by lazy {
Application.httpClient.newBuilder()
.callTimeout(15, TimeUnit.MINUTES)
.readTimeout(10, TimeUnit.MINUTES)
.proxySelector(ProxySelector.getDefault())
.build()
}
override fun ready() {
val databaseFile = Geo.getInstance().getDatabaseFile()
if (databaseFile.exists()) {
ready = true
return
}
// 重新加载
reload()
}
fun isReady() = ready
internal fun reload() {
ready = false
val databaseFile = Geo.getInstance().getDatabaseFile()
swingCoroutineScope.launch(Dispatchers.IO) {
var timeout = 3
while (ready.not()) {
try {
FileUtils.forceMkdirParent(databaseFile)
downloadGeoLite2(databaseFile)
withContext(Dispatchers.Swing) { GeoHostTreeShowMoreEnableExtension.instance.updateComponentTreeUI() }
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
delay(timeout.seconds)
timeout = timeout * 2
}
}
}
private fun downloadGeoLite2(dbFile: File) {
val url = "https://git.io/GeoLite2-Country.mmdb"
val response = httpClient.newCall(
Request.Builder().get().url(url)
.build()
).execute()
log.info("Fetched GeoLite2-Country.mmdb from {} status {}", url, response.code)
if (response.isSuccessful.not()) {
IOUtils.closeQuietly(response)
throw IllegalStateException("GeoLite2-Country.mmdb could not be downloaded, HTTP ${response.code}")
}
val body = response.body
val input = body?.byteStream()
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID())
val output = file.outputStream()
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
IOUtils.closeQuietly(input, output, body, response)
log.info("Downloaded GeoLite2-Country.mmdb from {} , result: {}", url, downloaded)
if (downloaded) {
FileUtils.moveFile(file, dbFile)
ready = true
} else {
throw IllegalStateException("GeoLite2-Country.mmdb could not be downloaded")
}
}
}

View File

@@ -1,47 +0,0 @@
package app.termora.plugins.geo
import app.termora.EnableManager
import app.termora.FrameExtension
import app.termora.OptionPane
import app.termora.TermoraFrame
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
class GeoFrameExtension private constructor() : FrameExtension {
companion object {
val instance = GeoFrameExtension()
private const val FIRST_KEY = "Plugins.Geo.isFirst"
}
private val enableManager get() = EnableManager.getInstance()
override fun customize(frame: TermoraFrame) {
// 已经加载完毕,那么不需要提示
if (GeoApplicationRunnerExtension.instance.isReady()) return
// 已经提示过了,直接退出
val isFirst = enableManager.getFlag(FIRST_KEY, true)
if (isFirst.not()) return
frame.addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
enableManager.setFlag(FIRST_KEY, false)
frame.removeWindowListener(this)
SwingUtilities.invokeLater { showMessageDialog(frame) }
}
})
}
private fun showMessageDialog(window: Window) {
OptionPane.showMessageDialog(
window,
GeoI18n.getString("termora.plugins.geo.first-message"),
messageType = JOptionPane.INFORMATION_MESSAGE
)
}
}

View File

@@ -1,7 +1,6 @@
package app.termora.plugins.geo package app.termora.plugins.geo
import app.termora.EnableManager import app.termora.EnableManager
import app.termora.I18n
import app.termora.SwingUtils import app.termora.SwingUtils
import app.termora.TermoraFrameManager import app.termora.TermoraFrameManager
import app.termora.tree.HostTreeShowMoreEnableExtension import app.termora.tree.HostTreeShowMoreEnableExtension
@@ -21,11 +20,7 @@ internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTr
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem { override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
val item = JCheckBoxMenuItem("Geo") val item = JCheckBoxMenuItem("Geo")
item.isEnabled = GeoApplicationRunnerExtension.instance.isReady()
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true) item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
if (item.isEnabled.not()) {
item.text = GeoI18n.getString("termora.plugins.geo.coming-soon")
}
item.addActionListener { item.addActionListener {
enableManager.setFlag(KEY, item.isSelected) enableManager.setFlag(KEY, item.isSelected)
updateComponentTreeUI() updateComponentTreeUI()

View File

@@ -1,7 +1,5 @@
package app.termora.plugins.geo package app.termora.plugins.geo
import app.termora.ApplicationRunnerExtension
import app.termora.FrameExtension
import app.termora.plugin.Extension import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin import app.termora.plugin.Plugin
@@ -12,10 +10,8 @@ class GeoPlugin : Plugin {
private val support = ExtensionSupport() private val support = ExtensionSupport()
init { init {
support.addExtension(ApplicationRunnerExtension::class.java) { GeoApplicationRunnerExtension.instance }
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance } support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance } support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
support.addExtension(FrameExtension::class.java) { GeoFrameExtension.instance }
} }

View File

@@ -1,6 +1,7 @@
package app.termora package app.termora
import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.* import java.util.*
@@ -33,6 +34,13 @@ object I18n : AbstractI18n() {
return null return null
} }
/**
* 如果使用 zh_CN 则定义为中国大陆用户,应当优先使用国内服务器
*/
fun isChinaMainland(): Boolean {
return StringUtils.equalsIgnoreCase(containsLanguage(Locale.getDefault()), "zh_CN")
}
fun getLanguages(): Map<String, String> { fun getLanguages(): Map<String, String> {
return supportedLanguages return supportedLanguages
} }

View File

@@ -93,8 +93,12 @@ internal class MarketplaceManager private constructor() {
val version = Semver.parse(Application.getVersion()) val version = Semver.parse(Application.getVersion())
?: return emptyList() ?: return emptyList()
val repositories = PluginRepositoryManager.getInstance().getRepositories().toMutableSet() val repositories = PluginRepositoryManager.getInstance().getRepositories().distinct().toMutableList()
repositories.add("https://github.com/TermoraDev/termora-marketplace/releases/latest/download/plugins.xml") if (I18n.isChinaMainland()) {
repositories.addFirst("https://plugins.termora.cn/plugins.xml")
} else {
repositories.addFirst("https://github.com/TermoraDev/termora-marketplace/releases/latest/download/plugins.xml")
}
val plugins = mutableListOf<MarketplacePlugin>() val plugins = mutableListOf<MarketplacePlugin>()
val executorService = Executors.newVirtualThreadPerTaskExecutor() val executorService = Executors.newVirtualThreadPerTaskExecutor()