chore!: migrate to version 2.x

This commit is contained in:
hstyi
2025-06-13 15:16:56 +08:00
committed by GitHub
parent ca484618c7
commit 6177bbdc68
444 changed files with 18594 additions and 3832 deletions

View File

@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,97 @@
package app.termora.plugins.geo
import app.termora.Application
import app.termora.ApplicationScope
import app.termora.Disposable
import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.net.InetAddress
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.optionals.getOrNull
internal class Geo private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(Geo::class.java)
fun getInstance(): Geo {
return ApplicationScope.forApplicationScope()
.getOrCreate(Geo::class) { Geo() }
}
}
private val initialized = AtomicBoolean(false)
private var reader: DatabaseReader? = null
private fun initialize() {
if (GeoApplicationRunnerExtension.instance.isReady().not()) return
if (isInitialized()) return
if (initialized.compareAndSet(false, true)) {
try {
val database = getDatabaseFile()
if ((database.exists() && database.isFile).not()) {
throw IllegalStateException("${database.absolutePath} not be found")
}
val locale = Locale.getDefault().toString().replace("_", "-")
try {
reader = DatabaseReader.Builder(database)
.locales(listOf(locale, "en"))
.withCache(CHMCache()).build()
} catch (e: Exception) {
// 打开数据失败一般都是数据文件顺坏,删除数据库
FileUtils.deleteQuietly(database)
// 重新下载
GeoApplicationRunnerExtension.instance.reload()
throw e
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("Failed to initialize geo database", e)
}
initialized.set(false)
}
}
}
fun getDatabaseFile(): File {
val dir = FileUtils.getFile(Application.getBaseDataDir(), "config", "plugins", "geo")
return File(dir, "GeoLite2-Country.mmdb")
}
fun country(ip: String): Country? {
try {
initialize()
val reader = reader ?: return null
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
val isoCode = response.country.isoCode
var name = response.country.name
// 控制名称不要太长如果太长则使用缩写。例如United States
if (name != null && name.length > 6) name = isoCode
return Country(isoCode, name ?: isoCode)
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error("Failed to initialize geo database", e)
}
return null
}
}
fun isInitialized(): Boolean = initialized.get()
override fun dispose() {
IOUtils.closeQuietly(reader)
}
data class Country(val isoCode: String, val name: String)
}

View File

@@ -0,0 +1,108 @@
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://github.com/P3TERX/GeoLite.mmdb/raw/download/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

@@ -0,0 +1,47 @@
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

@@ -0,0 +1,48 @@
package app.termora.plugins.geo
import app.termora.EnableManager
import app.termora.I18n
import app.termora.SwingUtils
import app.termora.TermoraFrameManager
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.NewHostTree
import javax.swing.JCheckBoxMenuItem
import javax.swing.JTree
import javax.swing.SwingUtilities
internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTreeShowMoreEnableExtension {
companion object {
private const val KEY = "Plugins.Geo.ShowMore.Enable"
val instance = GeoHostTreeShowMoreEnableExtension()
}
private val enableManager get() = EnableManager.getInstance()
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
val item = JCheckBoxMenuItem("Geo")
item.isEnabled = GeoApplicationRunnerExtension.instance.isReady()
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
if (item.isEnabled.not()) {
item.text = GeoI18n.getString("termora.plugins.geo.coming-soon")
}
item.addActionListener {
enableManager.setFlag(KEY, item.isSelected)
updateComponentTreeUI()
}
return item
}
fun updateComponentTreeUI() {
// reload all tree
for (frame in TermoraFrameManager.getInstance().getWindows()) {
for (tree in SwingUtils.getDescendantsOfClass(NewHostTree::class.java, frame)) {
SwingUtilities.updateComponentTreeUI(tree)
}
}
}
fun isShowMore(): Boolean {
return enableManager.getFlag(KEY, true)
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.geo
import app.termora.AbstractI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object GeoI18n : AbstractI18n() {
private val log = LoggerFactory.getLogger(GeoI18n::class.java)
private val myBundle by lazy {
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), GeoI18n::class.java.classLoader)
if (log.isInfoEnabled) {
log.info("I18n: {}", bundle.baseBundleName ?: "null")
}
return@lazy bundle
}
override fun getBundle(): ResourceBundle {
return myBundle
}
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,37 @@
package app.termora.plugins.geo
import app.termora.ApplicationRunnerExtension
import app.termora.FrameExtension
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.SimpleTreeCellRendererExtension
class GeoPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(ApplicationRunnerExtension::class.java) { GeoApplicationRunnerExtension.instance }
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
support.addExtension(FrameExtension::class.java) { GeoFrameExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Geo"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,58 @@
package app.termora.plugins.geo
import app.termora.ColorHash
import app.termora.tree.HostTreeNode
import app.termora.tree.MarkerSimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellRendererExtension
import java.awt.Color
import javax.swing.JTree
class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellRendererExtension {
companion object {
val instance = GeoSimpleTreeCellRendererExtension()
}
private val geo get() = Geo.getInstance()
override fun createAnnotations(
tree: JTree,
value: Any?,
sel: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean
): List<SimpleTreeCellAnnotation> {
val node = value as HostTreeNode? ?: return emptyList()
if (node.isFolder) return emptyList()
val protocol = node.data.protocol
if ((protocol == "SSH" || protocol == "RDP").not()) return emptyList()
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
val country = geo.country(node.data.host) ?: return emptyList()
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
return listOf(
MarkerSimpleTreeCellAnnotation(
text,
foreground = Color.white,
background = ColorHash.hash(country.isoCode),
)
)
}
private fun countryCodeToFlagEmoji(code: String): String {
if (code.length < 2) return ""
val upper = code.take(2).uppercase()
val first = Character.codePointAt(upper, 0) - 'A'.code + 0x1F1E6
val second = Character.codePointAt(upper, 1) - 'A'.code + 0x1F1E6
return String(Character.toChars(first)) + String(Character.toChars(second))
}
override fun ordered(): Long {
return 1
}
}

View File

@@ -0,0 +1,23 @@
<termora-plugin>
<id>geo</id>
<name>Geo</name>
<version>${projectVersion}</version>
<entry>app.termora.plugins.geo.GeoPlugin</entry>
<termora-version since=">=${rootProjectVersion}" until=""/>
<descriptions>
<description>Display the geographical location of the host</description>
<description language="zh_CN">显示主机的地理位置</description>
<description language="zh_TW">顯示主機的地理位置</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=The first time you use the <b>Geo</b> plugin, it will download the <b>GeoLite2.mmdb</b> database. <br/>Once the download is complete, it will display the host region information.
termora.plugins.geo.coming-soon=Geo loading

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
termora.plugins.geo.coming-soon=Geo 加载中

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
termora.plugins.geo.coming-soon=Geo 加载中