mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
chore!: migrate to version 2.x
This commit is contained in:
97
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal file
97
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
23
plugins/geo/src/main/resources/META-INF/plugin.xml
Normal file
23
plugins/geo/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
5
plugins/geo/src/main/resources/META-INF/pluginIcon.svg
Normal file
5
plugins/geo/src/main/resources/META-INF/pluginIcon.svg
Normal 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 |
@@ -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 |
2
plugins/geo/src/main/resources/i18n/messages.properties
Normal file
2
plugins/geo/src/main/resources/i18n/messages.properties
Normal 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
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
|
||||
termora.plugins.geo.coming-soon=Geo 加载中
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
|
||||
termora.plugins.geo.coming-soon=Geo 加载中
|
||||
Reference in New Issue
Block a user