chore!: migrate to version 2.x
10
plugins/LICENSE
Normal file
@@ -0,0 +1,10 @@
|
||||
Copyright (c) 2025-present hstyi
|
||||
|
||||
The files in this catalogue are for public access only. Specific descriptions are given below:
|
||||
|
||||
- You may view and study the contents of these files;
|
||||
- You may NOT use them for any commercial purpose;
|
||||
- You may NOT modify, copy, distribute, republish, or use them to create derivative works;
|
||||
- Written permission must be obtained from the author for any use beyond personal viewing.
|
||||
|
||||
All rights reserved.
|
||||
75
plugins/THIRDPARTY
Normal file
@@ -0,0 +1,75 @@
|
||||
minio
|
||||
Apache License 2.0
|
||||
https://github.com/minio/minio-java/blob/master/LICENSE
|
||||
|
||||
aliyun-sdk-oss
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
jaxb-api
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
|
||||
|
||||
activation
|
||||
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
|
||||
https://github.com/javaee/activation/blob/master/LICENSE.txt
|
||||
|
||||
jaxb-runtime
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
|
||||
|
||||
esdk-obs-java-bundle
|
||||
HUAWEI LICENSE
|
||||
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
|
||||
|
||||
xodus-compress
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-environment
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-openAPI
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-utils
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-vfs
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
kotlin-bip39
|
||||
MIT License
|
||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||
|
||||
commons-compress
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||
|
||||
cos_api
|
||||
MIT License
|
||||
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
|
||||
|
||||
AutoComplete
|
||||
BSD-3-Clause license
|
||||
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
|
||||
|
||||
RSTALanguageSupport
|
||||
BSD-3-Clause license
|
||||
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
|
||||
|
||||
RSyntaxTextArea
|
||||
BSD-3-Clause license
|
||||
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
|
||||
|
||||
MaxMind GeoIP2 API
|
||||
Apache License, Version 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
GeoLite2 (https://www.maxmind.com)
|
||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||
https://creativecommons.org/licenses/by-sa/4.0/
|
||||
16
plugins/bg/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.EnableManager
|
||||
import app.termora.database.DatabaseManager
|
||||
|
||||
object Appearance {
|
||||
private val enableManager get() = EnableManager.getInstance()
|
||||
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||
|
||||
var backgroundImage: String
|
||||
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
|
||||
set(value) {
|
||||
enableManager.setFlag("Plugins.bg.backgroundImage", value)
|
||||
}
|
||||
|
||||
var interval: Int
|
||||
get() = enableManager.getFlag("Plugins.bg.interval", 360)
|
||||
set(value) {
|
||||
enableManager.setFlag("Plugins.bg.interval", value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.GlassPaneExtension
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import java.awt.AlphaComposite
|
||||
import java.awt.Graphics2D
|
||||
import javax.swing.JComponent
|
||||
|
||||
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
||||
companion object {
|
||||
val instance = BGGlassPaneExtension()
|
||||
}
|
||||
|
||||
override fun paint(
|
||||
c: JComponent,
|
||||
g2d: Graphics2D
|
||||
): Boolean {
|
||||
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return false
|
||||
g2d.composite = AlphaComposite.getInstance(
|
||||
AlphaComposite.SRC_OVER,
|
||||
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||
)
|
||||
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
26
plugins/bg/src/main/kotlin/app/termora/plugins/bg/BGI18n.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.AbstractI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object BGI18n : AbstractI18n() {
|
||||
private val log = LoggerFactory.getLogger(BGI18n::class.java)
|
||||
private val myBundle by lazy {
|
||||
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::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,36 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
import app.termora.GlassPaneAwareExtension
|
||||
import app.termora.GlassPaneExtension
|
||||
import app.termora.SettingsOptionExtension
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
|
||||
class BGPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
|
||||
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
|
||||
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Customize Background"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Window
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||
ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||
fun getInstance(): BackgroundManager {
|
||||
return ApplicationScope.Companion.forApplicationScope()
|
||||
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private var bufferedImage: BufferedImage? = null
|
||||
private var imageFilepath = StringUtils.EMPTY
|
||||
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||
|
||||
|
||||
fun setBackgroundImage(url: String) {
|
||||
clearBackgroundImage()
|
||||
Appearance.backgroundImage = url
|
||||
refreshBackgroundImage()
|
||||
}
|
||||
|
||||
fun getBackgroundImage(): BufferedImage? {
|
||||
val bg = doGetBackgroundImage()
|
||||
if (bg == null) {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
return null
|
||||
} else {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||
}
|
||||
} else {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||
}
|
||||
}
|
||||
return bg
|
||||
}
|
||||
|
||||
private fun doGetBackgroundImage(): BufferedImage? {
|
||||
synchronized(this) {
|
||||
return bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBackgroundImage() {
|
||||
synchronized(this) {
|
||||
bufferedImage = null
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
Appearance.backgroundImage = StringUtils.EMPTY
|
||||
}
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshBackgroundImage() {
|
||||
val backgroundImage = Appearance.backgroundImage
|
||||
if (backgroundImage.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
var file: File? = null
|
||||
|
||||
// 从网络下载
|
||||
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||
file = Application.httpClient.newCall(
|
||||
Request.Builder().get()
|
||||
.url(backgroundImage).build()
|
||||
).execute().use { response ->
|
||||
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||
if (response.isSuccessful.not()) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||
}
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
if (body != null) {
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
}
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundImageFile = File(backgroundImage)
|
||||
if (backgroundImageFile.isDirectory) {
|
||||
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||
if (files.isNotEmpty()) {
|
||||
for (i in 0 until files.size) {
|
||||
file = files.randomOrNull()
|
||||
if (file == null) break
|
||||
if (file.absolutePath == imageFilepath) continue
|
||||
}
|
||||
} else {
|
||||
synchronized(this) {
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
bufferedImage = null
|
||||
refreshGlassPanes()
|
||||
}
|
||||
}
|
||||
} else if (backgroundImageFile.isFile) {
|
||||
file = backgroundImageFile
|
||||
}
|
||||
|
||||
if (file == null || imageFilepath == file.absolutePath) {
|
||||
return
|
||||
}
|
||||
|
||||
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||
imageFilepath = file.absolutePath
|
||||
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshGlassPanes() {
|
||||
SwingUtilities.invokeLater {
|
||||
glassPanes.removeIf {
|
||||
val glassPane = it.get()
|
||||
glassPane?.repaint()
|
||||
glassPane == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
}
|
||||
|
||||
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||
glassPanes.add(WeakReference(glassPane))
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
runCatching { refreshBackgroundImage() }.onFailure {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Refresh failed", it)
|
||||
}
|
||||
}
|
||||
delay(max(Appearance.interval, 30).seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.nv.FileChooser
|
||||
import com.formdev.flatlaf.extras.components.FlatButton
|
||||
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.io.File
|
||||
import java.nio.file.StandardCopyOption
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
|
||||
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
|
||||
}
|
||||
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
val backgroundImageTextField = OutlineTextField()
|
||||
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||
|
||||
private val backgroundButton = JButton(Icons.folder)
|
||||
private val backgroundClearButton = FlatButton()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
backgroundImageTextField.isEditable = false
|
||||
backgroundImageTextField.trailingComponent = backgroundButton
|
||||
backgroundImageTextField.text = Appearance.backgroundImage
|
||||
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
}
|
||||
})
|
||||
|
||||
backgroundClearButton.isFocusable = false
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
backgroundClearButton.icon = Icons.delete
|
||||
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||
|
||||
intervalSpinner.value = Appearance.interval
|
||||
|
||||
add(getFormPanel(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
backgroundButton.addActionListener {
|
||||
val chooser = FileChooser()
|
||||
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
|
||||
chooser.showOpenDialog(owner).thenAccept {
|
||||
if (it.isNotEmpty()) {
|
||||
onSelectedBackgroundImage(it.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backgroundClearButton.addActionListener {
|
||||
BackgroundManager.getInstance().clearBackgroundImage()
|
||||
backgroundImageTextField.text = StringUtils.EMPTY
|
||||
}
|
||||
|
||||
intervalSpinner.addChangeListener {
|
||||
val value = intervalSpinner.value
|
||||
if (value is Int) {
|
||||
Appearance.interval = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedBackgroundImage(file: File) {
|
||||
try {
|
||||
if (file.isFile) {
|
||||
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||
FileUtils.forceMkdirParent(destFile)
|
||||
FileUtils.deleteQuietly(destFile)
|
||||
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
|
||||
} else if (file.isDirectory) {
|
||||
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
|
||||
}
|
||||
backgroundImageTextField.text = file.absolutePath
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.imageGray
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return BGI18n.getString("termora.plugins.bg.background-image")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val builder = FormBuilder.create().layout(layout)
|
||||
val bgClearBox = Box.createHorizontalBox()
|
||||
bgClearBox.add(backgroundClearButton)
|
||||
|
||||
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
|
||||
.add(backgroundImageTextField).xy(3, rows)
|
||||
.add(bgClearBox).xy(5, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||
.add(intervalSpinner).xy(3, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.OptionsPane
|
||||
import app.termora.SettingsOptionExtension
|
||||
|
||||
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
|
||||
companion object {
|
||||
val instance by lazy { BackgroundSettingsOptionExtension() }
|
||||
}
|
||||
|
||||
override fun createSettingsOption(): OptionsPane.Option {
|
||||
return BackgroundOption()
|
||||
}
|
||||
}
|
||||
23
plugins/bg/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>bg</id>
|
||||
|
||||
<name>Customize Background</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<entry>app.termora.plugins.bg.BGPlugin</entry>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
|
||||
<descriptions>
|
||||
<description>Customize application background</description>
|
||||
<description language="zh_CN">自定义应用程序背景</description>
|
||||
<description language="zh_TW">自訂應用程式背景</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
6
plugins/bg/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- 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">
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
|
||||
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
|
||||
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,6 @@
|
||||
<!-- 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">
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
|
||||
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
|
||||
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 472 B |
2
plugins/bg/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.interval=Interval
|
||||
termora.plugins.bg.background-image=Background Image
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.background-image=背景图
|
||||
termora.plugins.bg.interval=切换间隔
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.background-image=背景圖
|
||||
termora.plugins.bg.interval=切換間隔
|
||||
89
plugins/common.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
|
||||
|
||||
tasks.withType<Jar> {
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Implementation-Title" to project.name,
|
||||
"Implementation-Version" to project.version,
|
||||
)
|
||||
}
|
||||
|
||||
from("${rootProject.projectDir}/plugins/LICENSE") {
|
||||
into("META-INF")
|
||||
}
|
||||
|
||||
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
|
||||
into("META-INF")
|
||||
}
|
||||
|
||||
// archiveBaseName.set("${project.name}-${rootProject.version}")
|
||||
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
|
||||
}
|
||||
|
||||
tasks.named<Copy>("processResources") {
|
||||
filesMatching("META-INF/plugin.xml") {
|
||||
expand(
|
||||
"projectName" to project.name,
|
||||
"projectVersion" to project.version,
|
||||
"rootProjectVersion" to rootProject.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copy-dependencies") {
|
||||
from(configurations.getByName("runtimeClasspath").filterNot {
|
||||
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
|
||||
})
|
||||
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
|
||||
}
|
||||
|
||||
tasks.named("build") {
|
||||
dependsOn("copy-dependencies")
|
||||
}
|
||||
|
||||
tasks.register("run-plugin") {
|
||||
dependsOn("build")
|
||||
|
||||
doLast {
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
|
||||
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
|
||||
val mainClass = "app.termora.MainKt"
|
||||
val executable = System.getProperty("java.home") + "/bin/java"
|
||||
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
|
||||
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
|
||||
val commands = mutableListOf<String>(executable)
|
||||
commands.add("-Dapp-version=${rootProject.version}")
|
||||
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
|
||||
if (os.isMacOsX) {
|
||||
// NSWindow
|
||||
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
commands.add("-Dapple.awt.application.appearance=system")
|
||||
}
|
||||
commands.addAll(listOf("-cp", classpath, mainClass))
|
||||
|
||||
exec {
|
||||
commandLine = commands
|
||||
environment(
|
||||
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
|
||||
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.named("clean") {
|
||||
doLast {
|
||||
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
|
||||
}
|
||||
}
|
||||
16
plugins/cos/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.qcloud:cos_api:5.6.245")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class COSFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { COSFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return COSFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class COSPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { COSProtocolProviderExtension.Companion.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.Companion.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Tencent COS"
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class COSProtocolHostPanel : ProtocolHostPanel() {
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = COSProtocolProvider.Companion.PROTOCOL
|
||||
)
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { COSProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return COSProtocolProvider.Companion.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return COSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class COSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { COSProtocolProvider() }
|
||||
const val PROTOCOL = "COS"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.tencent
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return COSFileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { COSProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return COSProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
25
plugins/cos/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>cos</id>
|
||||
|
||||
<name>Tencent COS</name>
|
||||
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.cos.COSPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to Tencent COS</description>
|
||||
<description language="zh_CN">支持连接到腾讯云对象存储</description>
|
||||
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/cos/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
19
plugins/editor/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||
implementation("com.fifesoft:languagesupport:3.3.0")
|
||||
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.OptionPane
|
||||
import app.termora.sftp.absolutePathString
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
|
||||
|
||||
class EditorDialog(file: FileObject, owner: Window, myDisposable: Disposable) : DialogWrapper(null) {
|
||||
|
||||
private val filename = file.name.baseName
|
||||
private val filepath = File(file.absolutePathString())
|
||||
private val editorPanel = EditorPanel(this, filepath)
|
||||
|
||||
init {
|
||||
Disposer.register(disposable, myDisposable)
|
||||
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = false
|
||||
controlsVisible = true
|
||||
isResizable = true
|
||||
title = filename
|
||||
iconImages = owner.iconImages
|
||||
escapeDispose = false
|
||||
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
initEvents()
|
||||
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent?) {
|
||||
doCancelAction()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
if (editorPanel.changes()) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
this,
|
||||
"文件尚未保存,你确定要退出吗?",
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||
) != JOptionPane.OK_OPTION
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return editorPanel
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.DocumentAdaptor
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.Icons
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
|
||||
import org.fife.ui.rsyntaxtextarea.Theme
|
||||
import org.fife.ui.rtextarea.RTextScrollPane
|
||||
import org.fife.ui.rtextarea.SearchContext
|
||||
import org.fife.ui.rtextarea.SearchEngine
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.math.max
|
||||
|
||||
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
|
||||
private var text = file.readText(Charsets.UTF_8)
|
||||
private val layeredPane = LayeredPane()
|
||||
|
||||
private val textArea = RSyntaxTextArea()
|
||||
private val scrollPane = RTextScrollPane(textArea)
|
||||
private val findPanel = FlatToolBar().apply { isFloatable = false }
|
||||
private val searchTextField = FlatTextField()
|
||||
private val closeFindPanelBtn = JButton(Icons.close)
|
||||
private val nextBtn = JButton(Icons.down)
|
||||
private val prevBtn = JButton(Icons.up)
|
||||
private val context = SearchContext()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
|
||||
textArea.text = text
|
||||
textArea.antiAliasingEnabled = true
|
||||
|
||||
val theme = if (FlatLaf.isLafDark())
|
||||
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
|
||||
else
|
||||
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
|
||||
|
||||
theme.apply(textArea)
|
||||
|
||||
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
|
||||
textArea.syntaxEditingStyle = when (extension) {
|
||||
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
|
||||
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
|
||||
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
|
||||
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
|
||||
"c" -> SyntaxConstants.SYNTAX_STYLE_C
|
||||
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
|
||||
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
|
||||
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
|
||||
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
|
||||
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
|
||||
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
|
||||
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
|
||||
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
|
||||
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
|
||||
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
|
||||
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
|
||||
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
|
||||
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
|
||||
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
|
||||
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
|
||||
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
|
||||
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
|
||||
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
|
||||
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
|
||||
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
|
||||
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
|
||||
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
|
||||
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
|
||||
else -> SyntaxConstants.SYNTAX_STYLE_NONE
|
||||
}
|
||||
|
||||
textArea.discardAllEdits()
|
||||
|
||||
scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
findPanel.isVisible = false
|
||||
findPanel.isOpaque = true
|
||||
findPanel.background = DynamicColor("window")
|
||||
|
||||
searchTextField.background = findPanel.background
|
||||
searchTextField.padding = Insets(0, 4, 0, 0)
|
||||
searchTextField.border = BorderFactory.createEmptyBorder()
|
||||
|
||||
findPanel.add(searchTextField)
|
||||
findPanel.add(prevBtn)
|
||||
findPanel.add(nextBtn)
|
||||
findPanel.add(closeFindPanelBtn)
|
||||
findPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||
)
|
||||
|
||||
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
|
||||
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
|
||||
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
window.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent?) {
|
||||
scrollPane.verticalScrollBar.value = 0
|
||||
window.removeWindowListener(this)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
textArea.inputMap.put(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
|
||||
"Save"
|
||||
)
|
||||
textArea.inputMap.put(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
|
||||
"Find"
|
||||
)
|
||||
|
||||
searchTextField.inputMap.put(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
"Esc"
|
||||
)
|
||||
|
||||
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
textArea.clearMarkAllHighlights()
|
||||
textArea.requestFocusInWindow()
|
||||
findPanel.isVisible = false
|
||||
}
|
||||
})
|
||||
|
||||
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
|
||||
|
||||
textArea.actionMap.put("Save", object : AbstractAction("Save") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
file.writeText(textArea.text, Charsets.UTF_8)
|
||||
text = textArea.text
|
||||
window.title = file.name
|
||||
}
|
||||
})
|
||||
|
||||
textArea.actionMap.put("Find", object : AbstractAction("Find") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
findPanel.isVisible = true
|
||||
searchTextField.selectAll()
|
||||
searchTextField.requestFocusInWindow()
|
||||
}
|
||||
})
|
||||
|
||||
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
window.title = if (textArea.text.hashCode() != text.hashCode()) {
|
||||
"${file.name} *"
|
||||
} else {
|
||||
file.name
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
search()
|
||||
}
|
||||
})
|
||||
|
||||
searchTextField.addActionListener { nextBtn.doClick(0) }
|
||||
|
||||
prevBtn.addActionListener { search(false) }
|
||||
nextBtn.addActionListener { search(true) }
|
||||
}
|
||||
|
||||
private fun search(searchForward: Boolean = true) {
|
||||
textArea.clearMarkAllHighlights()
|
||||
|
||||
|
||||
val text: String = searchTextField.getText()
|
||||
if (text.isEmpty()) return
|
||||
context.searchFor = text
|
||||
context.searchForward = searchForward
|
||||
context.wholeWord = false
|
||||
val result = SearchEngine.find(textArea, context)
|
||||
|
||||
prevBtn.isEnabled = result.markedCount > 0
|
||||
nextBtn.isEnabled = result.markedCount > 0
|
||||
|
||||
}
|
||||
|
||||
fun changes() = text != textArea.text
|
||||
|
||||
private inner class LayeredPane : JLayeredPane() {
|
||||
override fun doLayout() {
|
||||
synchronized(treeLock) {
|
||||
for (c in components) {
|
||||
if (c == findPanel) {
|
||||
val height = max(findPanel.preferredSize.height, findPanel.height)
|
||||
val x = width / 2
|
||||
c.setBounds(x, 1, width - x, height)
|
||||
} else {
|
||||
c.setBounds(0, 0, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
import app.termora.sftp.SFTPEditFileExtension
|
||||
|
||||
class EditorPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(SFTPEditFileExtension::class.java) { MySFTPEditFileExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "SFTP File Editor"
|
||||
}
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.sftp.SFTPEditFileExtension
|
||||
import app.termora.sftp.absolutePathString
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import java.awt.Window
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class MySFTPEditFileExtension private constructor() : SFTPEditFileExtension {
|
||||
companion object {
|
||||
val instance = MySFTPEditFileExtension()
|
||||
}
|
||||
|
||||
override fun edit(owner: Window, file: FileObject): Disposable {
|
||||
val disposable = Disposer.newDisposable()
|
||||
SwingUtilities.invokeLater { EditorDialog(file, owner, disposable).isVisible = true }
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
22
plugins/editor/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>editor</id>
|
||||
|
||||
<name>SFTP File Editor</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.editor.EditorPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Edit SFTP files using the built-in editor</description>
|
||||
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
|
||||
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- 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">
|
||||
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
||||
<!-- 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">
|
||||
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,107 @@
|
||||
package app.termora.plugins.editor;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import javax.swing.*;
|
||||
|
||||
import org.fife.ui.rtextarea.*;
|
||||
import org.fife.ui.rsyntaxtextarea.*;
|
||||
|
||||
/**
|
||||
* A simple example showing how to do search and replace in a RSyntaxTextArea.
|
||||
* The toolbar isn't very user-friendly, but this is just to show you how to use
|
||||
* the API.<p>
|
||||
*
|
||||
* This example uses RSyntaxTextArea 2.5.6.
|
||||
*/
|
||||
public class FindAndReplaceDemo extends JFrame implements ActionListener {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private RSyntaxTextArea textArea;
|
||||
private JTextField searchField;
|
||||
private JCheckBox regexCB;
|
||||
private JCheckBox matchCaseCB;
|
||||
|
||||
public FindAndReplaceDemo() {
|
||||
|
||||
JPanel cp = new JPanel(new BorderLayout());
|
||||
|
||||
textArea = new RSyntaxTextArea(20, 60);
|
||||
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
|
||||
textArea.setCodeFoldingEnabled(true);
|
||||
RTextScrollPane sp = new RTextScrollPane(textArea);
|
||||
cp.add(sp);
|
||||
|
||||
// Create a toolbar with searching options.
|
||||
JToolBar toolBar = new JToolBar();
|
||||
searchField = new JTextField(30);
|
||||
toolBar.add(searchField);
|
||||
final JButton nextButton = new JButton("Find Next");
|
||||
nextButton.setActionCommand("FindNext");
|
||||
nextButton.addActionListener(this);
|
||||
toolBar.add(nextButton);
|
||||
searchField.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
nextButton.doClick(0);
|
||||
}
|
||||
});
|
||||
JButton prevButton = new JButton("Find Previous");
|
||||
prevButton.setActionCommand("FindPrev");
|
||||
prevButton.addActionListener(this);
|
||||
toolBar.add(prevButton);
|
||||
regexCB = new JCheckBox("Regex");
|
||||
toolBar.add(regexCB);
|
||||
matchCaseCB = new JCheckBox("Match Case");
|
||||
toolBar.add(matchCaseCB);
|
||||
cp.add(toolBar, BorderLayout.NORTH);
|
||||
|
||||
setContentPane(cp);
|
||||
setTitle("Find and Replace Demo");
|
||||
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
}
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
|
||||
// "FindNext" => search forward, "FindPrev" => search backward
|
||||
String command = e.getActionCommand();
|
||||
boolean forward = "FindNext".equals(command);
|
||||
|
||||
// Create an object defining our search parameters.
|
||||
SearchContext context = new SearchContext();
|
||||
String text = searchField.getText();
|
||||
if (text.length() == 0) {
|
||||
return;
|
||||
}
|
||||
context.setSearchFor(text);
|
||||
context.setMatchCase(matchCaseCB.isSelected());
|
||||
context.setRegularExpression(regexCB.isSelected());
|
||||
context.setSearchForward(forward);
|
||||
context.setWholeWord(false);
|
||||
|
||||
boolean found = SearchEngine.find(textArea, context).wasFound();
|
||||
if (!found) {
|
||||
JOptionPane.showMessageDialog(this, "Text not found");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Start all Swing applications on the EDT.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
String laf = UIManager.getSystemLookAndFeelClassName();
|
||||
UIManager.setLookAndFeel(laf);
|
||||
} catch (Exception e) { /* never happens */ }
|
||||
FindAndReplaceDemo demo = new FindAndReplaceDemo();
|
||||
demo.setVisible(true);
|
||||
demo.textArea.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
15
plugins/ftp/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "FTP"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
24
plugins/ftp/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>ftp</id>
|
||||
|
||||
<name>FTP</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to FTP</description>
|
||||
<description language="zh_CN">支持连接到到 FTP</description>
|
||||
<description language="zh_TW">支援連接到 FTP</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/ftp/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
14
plugins/geo/build.gradle.kts
Normal 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")
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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 加载中
|
||||
22
plugins/migration/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.commons.compress)
|
||||
}
|
||||
|
||||
|
||||
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,746 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
import jetbrains.exodus.env.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class Database private constructor(private val env: Environment) : Disposable {
|
||||
companion object {
|
||||
private const val KEYMAP_STORE = "Keymap"
|
||||
private const val HOST_STORE = "Host"
|
||||
private const val SNIPPET_STORE = "Snippet"
|
||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||
private const val MACRO_STORE = "Macro"
|
||||
private const val KEY_PAIR_STORE = "KeyPair"
|
||||
private const val DELETED_DATA_STORE = "DeletedData"
|
||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||
|
||||
|
||||
private fun open(dir: File): Database {
|
||||
val config = EnvironmentConfig()
|
||||
// 32MB
|
||||
config.setLogFileSize(1024 * 32)
|
||||
config.setGcEnabled(true)
|
||||
// 5m
|
||||
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
|
||||
val environment = Environments.newInstance(dir, config)
|
||||
return Database(environment)
|
||||
}
|
||||
|
||||
fun getDatabase(): Database {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
|
||||
}
|
||||
}
|
||||
|
||||
val properties by lazy { Properties() }
|
||||
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
||||
val terminal by lazy { Terminal() }
|
||||
val appearance by lazy { Appearance() }
|
||||
val sftp by lazy { SFTP() }
|
||||
val sync by lazy { Sync() }
|
||||
|
||||
private val doorman get() = Doorman.getInstance()
|
||||
|
||||
|
||||
fun getKeymaps(): Collection<Keymap> {
|
||||
val array = env.computeInTransaction { tx ->
|
||||
openCursor<String>(tx, KEYMAP_STORE) { _, value ->
|
||||
value
|
||||
}.values
|
||||
}
|
||||
|
||||
val keymaps = mutableListOf<Keymap>()
|
||||
for (text in array.iterator()) {
|
||||
keymaps.add(Keymap.fromJSON(text) ?: continue)
|
||||
}
|
||||
|
||||
return keymaps
|
||||
}
|
||||
|
||||
fun addKeymap(keymap: Keymap) {
|
||||
env.executeInTransaction {
|
||||
put(it, KEYMAP_STORE, keymap.name, keymap.toJSON())
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Keymap: ${keymap.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeymap(name: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEYMAP_STORE, name)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Keymap: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getHosts(): Collection<Host> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Host>(tx, HOST_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllKeyPair() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
it.deleteCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeyPairs(): Collection<OhKeyPair> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<OhKeyPair>(tx, KEY_PAIR_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addHost(host: Host) {
|
||||
var text = ohMyJson.encodeToString(host)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, HOST_STORE, host.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Host: ${host.id} , ${host.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, HOST_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed host: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addDeletedData(deletedData: DeletedData) {
|
||||
val text = ohMyJson.encodeToString(deletedData)
|
||||
env.executeInTransaction {
|
||||
put(it, DELETED_DATA_STORE, deletedData.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added DeletedData: ${deletedData.id} , $text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDeletedData(): Collection<DeletedData> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<DeletedData?>(tx, DELETED_DATA_STORE) { _, value ->
|
||||
try {
|
||||
ohMyJson.decodeFromString(value)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}.values.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
var text = ohMyJson.encodeToString(snippet)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, SNIPPET_STORE, snippet.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, SNIPPET_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed snippet: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSnippets(): Collection<Snippet> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
||||
val text = ohMyJson.encodeToString(keywordHighlight)
|
||||
env.executeInTransaction {
|
||||
put(it, KEYWORD_HIGHLIGHT_STORE, keywordHighlight.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added keyword highlight: ${keywordHighlight.id} , ${keywordHighlight.keyword}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeywordHighlight(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEYWORD_HIGHLIGHT_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed keyword highlight: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMacros(): Collection<Macro> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Macro>(tx, MACRO_STORE) { _, value ->
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun addMacro(macro: Macro) {
|
||||
val text = ohMyJson.encodeToString(macro)
|
||||
env.executeInTransaction {
|
||||
put(it, MACRO_STORE, macro.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added macro: ${macro.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMacro(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, MACRO_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed macro: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeyPair(key: OhKeyPair) {
|
||||
var text = ohMyJson.encodeToString(key)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, KEY_PAIR_STORE, key.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Key Pair: ${key.id} , ${key.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeKeyPair(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, KEY_PAIR_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Key Pair: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun put(tx: Transaction, name: String, key: String, value: String) {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
val v = StringBinding.stringToEntry(value)
|
||||
store.put(tx, k, v)
|
||||
}
|
||||
|
||||
private fun delete(tx: Transaction, name: String, key: String) {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
store.delete(tx, k)
|
||||
}
|
||||
|
||||
fun getSafetyProperties(): List<SafetyProperties> {
|
||||
return listOf(sync, safetyProperties)
|
||||
}
|
||||
|
||||
private inline fun <reified T> openCursor(
|
||||
tx: Transaction,
|
||||
name: String,
|
||||
callback: (key: String, value: String) -> T
|
||||
): Map<String, T> {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
val map = mutableMapOf<String, T>()
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
try {
|
||||
val key = StringBinding.entryToString(it.key)
|
||||
map[key] = callback.invoke(
|
||||
key,
|
||||
StringBinding.entryToString(it.value)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Decode data failed. data: {}", it.value, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun putString(name: String, map: Map<String, String>) {
|
||||
return env.computeInTransaction {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, it)
|
||||
for ((key, value) in map.entries) {
|
||||
store.put(it, StringBinding.stringToEntry(key), StringBinding.stringToEntry(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getString(name: String, key: String): String? {
|
||||
return env.computeInTransaction {
|
||||
val store = env.openStore(name, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, it)
|
||||
val value = store.get(it, StringBinding.stringToEntry(key))
|
||||
if (value == null) null else StringBinding.entryToString(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract inner class Property(val name: String) {
|
||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||
|
||||
init {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||
}
|
||||
|
||||
protected open fun getString(key: String): String? {
|
||||
if (properties.containsKey(key)) {
|
||||
return properties[key]
|
||||
}
|
||||
return getString(name, key)
|
||||
}
|
||||
|
||||
open fun getProperties(): Map<String, String> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<String>(
|
||||
tx,
|
||||
name
|
||||
) { _, value -> value }
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun putString(key: String, value: String) {
|
||||
properties[key] = value
|
||||
putString(name, mapOf(key to value))
|
||||
}
|
||||
|
||||
|
||||
protected abstract inner class PropertyLazyDelegate<T>(protected val initializer: () -> T) :
|
||||
ReadWriteProperty<Any?, T> {
|
||||
private var value: T? = null
|
||||
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
if (value == null) {
|
||||
val v = getString(property.name)
|
||||
value = if (v == null) {
|
||||
initializer.invoke()
|
||||
} else {
|
||||
convertValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = initializer.invoke()
|
||||
}
|
||||
return value!!
|
||||
}
|
||||
|
||||
abstract fun convertValue(value: String): T
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
this.value = value
|
||||
putString(property.name, value.toString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract inner class PropertyDelegate<T>(private val defaultValue: T) :
|
||||
PropertyLazyDelegate<T>({ defaultValue })
|
||||
|
||||
|
||||
protected inner class StringPropertyDelegate(defaultValue: String) :
|
||||
PropertyDelegate<String>(defaultValue) {
|
||||
override fun convertValue(value: String): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class IntPropertyDelegate(defaultValue: Int) :
|
||||
PropertyDelegate<Int>(defaultValue) {
|
||||
override fun convertValue(value: String): Int {
|
||||
return value.toIntOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class DoublePropertyDelegate(defaultValue: Double) :
|
||||
PropertyDelegate<Double>(defaultValue) {
|
||||
override fun convertValue(value: String): Double {
|
||||
return value.toDoubleOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
||||
PropertyDelegate<Long>(defaultValue) {
|
||||
override fun convertValue(value: String): Long {
|
||||
return value.toLongOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class BooleanPropertyDelegate(defaultValue: Boolean) :
|
||||
PropertyDelegate<Boolean>(defaultValue) {
|
||||
override fun convertValue(value: String): Boolean {
|
||||
return value.toBooleanStrictOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
protected open inner class StringPropertyLazyDelegate(initializer: () -> String) :
|
||||
PropertyLazyDelegate<String>(initializer) {
|
||||
override fun convertValue(value: String): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
||||
PropertyDelegate<CursorStyle>(defaultValue) {
|
||||
override fun convertValue(value: String): CursorStyle {
|
||||
return try {
|
||||
CursorStyle.valueOf(value)
|
||||
} catch (_: Exception) {
|
||||
initializer.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class SyncTypePropertyDelegate(defaultValue: SyncType) :
|
||||
PropertyDelegate<SyncType>(defaultValue) {
|
||||
override fun convertValue(value: String): SyncType {
|
||||
try {
|
||||
return SyncType.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
return initializer.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 终端设置
|
||||
*/
|
||||
inner class Terminal : Property("Setting.Terminal") {
|
||||
|
||||
/**
|
||||
* 字体
|
||||
*/
|
||||
var font by StringPropertyDelegate("JetBrains Mono")
|
||||
|
||||
/**
|
||||
* 默认终端
|
||||
*/
|
||||
var localShell by StringPropertyLazyDelegate { Application.getDefaultShell() }
|
||||
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
var fontSize by IntPropertyDelegate(14)
|
||||
|
||||
/**
|
||||
* 最大行数
|
||||
*/
|
||||
var maxRows by IntPropertyDelegate(5000)
|
||||
|
||||
/**
|
||||
* 调试模式
|
||||
*/
|
||||
var debug by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 蜂鸣声
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
var cursorBlink by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 选中复制
|
||||
*/
|
||||
var selectCopy by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 光标样式
|
||||
*/
|
||||
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
||||
|
||||
/**
|
||||
* 终端断开连接时自动关闭Tab
|
||||
*/
|
||||
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 是否显示悬浮工具栏
|
||||
*/
|
||||
var floatingToolbar by BooleanPropertyDelegate(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用属性
|
||||
*/
|
||||
inner class Properties : Property("Setting.Properties") {
|
||||
public override fun getString(key: String): String? {
|
||||
return super.getString(key)
|
||||
}
|
||||
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
return getString(key) ?: defaultValue
|
||||
}
|
||||
|
||||
public override fun putString(key: String, value: String) {
|
||||
super.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全的通用属性
|
||||
*/
|
||||
open inner class SafetyProperties(name: String) : Property(name) {
|
||||
private val doorman get() = Doorman.getInstance()
|
||||
|
||||
public override fun getString(key: String): String? {
|
||||
var value = super.getString(key)
|
||||
if (value != null && doorman.isWorking()) {
|
||||
try {
|
||||
value = doorman.decrypt(value)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("decryption key: [{}], value: [{}] failed: {}", key, value, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
override fun getProperties(): Map<String, String> {
|
||||
val properties = super.getProperties()
|
||||
val map = mutableMapOf<String, String>()
|
||||
if (doorman.isWorking()) {
|
||||
for ((k, v) in properties) {
|
||||
try {
|
||||
map[k] = doorman.decrypt(v)
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("decryption key: [{}], value: [{}] failed: {}", k, v, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
map.putAll(properties)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
return getString(key) ?: defaultValue
|
||||
}
|
||||
|
||||
public override fun putString(key: String, value: String) {
|
||||
val v = if (doorman.isWorking()) doorman.encrypt(value) else value
|
||||
super.putString(key, v)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 外观
|
||||
*/
|
||||
inner class Appearance : Property("Setting.Appearance") {
|
||||
|
||||
|
||||
/**
|
||||
* 外观
|
||||
*/
|
||||
var theme by StringPropertyDelegate("Light")
|
||||
|
||||
/**
|
||||
* 跟随系统
|
||||
*/
|
||||
var followSystem by BooleanPropertyDelegate(true)
|
||||
var darkTheme by StringPropertyDelegate("Dark")
|
||||
var lightTheme by StringPropertyDelegate("Light")
|
||||
|
||||
/**
|
||||
* 允许后台运行,也就是托盘
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 标签关闭前确认
|
||||
*/
|
||||
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
var language by StringPropertyLazyDelegate {
|
||||
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 透明度
|
||||
*/
|
||||
var opacity by DoublePropertyDelegate(1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* SFTP
|
||||
*/
|
||||
inner class SFTP : Property("Setting.SFTP") {
|
||||
|
||||
|
||||
/**
|
||||
* 编辑命令
|
||||
*/
|
||||
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* sftp command
|
||||
*/
|
||||
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* defaultDirectory
|
||||
*/
|
||||
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* 是否固定在标签栏
|
||||
*/
|
||||
var pinTab by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 是否保留原始文件时间
|
||||
*/
|
||||
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步配置
|
||||
*/
|
||||
inner class Sync : SafetyProperties("Setting.Sync") {
|
||||
/**
|
||||
* 同步类型
|
||||
*/
|
||||
var type by SyncTypePropertyDelegate(SyncType.GitHub)
|
||||
|
||||
/**
|
||||
* 范围
|
||||
*/
|
||||
var rangeHosts by BooleanPropertyDelegate(true)
|
||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||
var rangeMacros by BooleanPropertyDelegate(true)
|
||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* Token
|
||||
*/
|
||||
var token by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* Gist ID
|
||||
*/
|
||||
var gist by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* Domain
|
||||
*/
|
||||
var domain by StringPropertyDelegate(String())
|
||||
|
||||
/**
|
||||
* 最后同步时间
|
||||
*/
|
||||
var lastSyncTime by LongPropertyDelegate(0L)
|
||||
|
||||
/**
|
||||
* 同步策略,为空就是默认手动
|
||||
*/
|
||||
var policy by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
IOUtils.closeQuietly(env)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.extras.components.FlatButton
|
||||
import com.formdev.flatlaf.extras.components.FlatLabel
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXHyperlink
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(DoormanDialog::class.java)
|
||||
}
|
||||
|
||||
private val formMargin = "7dlu"
|
||||
private val label = FlatLabel()
|
||||
private val icon = JLabel()
|
||||
private val passwordTextField = OutlinePasswordField()
|
||||
private val tip = FlatLabel()
|
||||
private val safeBtn = FlatButton()
|
||||
|
||||
var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
|
||||
label.labelType = FlatLabel.LabelType.h2
|
||||
label.horizontalAlignment = SwingConstants.CENTER
|
||||
safeBtn.isFocusable = false
|
||||
tip.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
|
||||
|
||||
safeBtn.addActionListener { doOKAction() }
|
||||
passwordTextField.addActionListener { doOKAction() }
|
||||
|
||||
var rows = 2
|
||||
val step = 2
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(label).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
}
|
||||
}
|
||||
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun showMnemonicsDialog() {
|
||||
val dialog = MnemonicsDialog(this@DoormanDialog)
|
||||
dialog.isVisible = true
|
||||
val entropy = dialog.entropy
|
||||
if (entropy.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyBackup = Database.getDatabase()
|
||||
.properties.getString("doorman-key-backup")
|
||||
?: throw IllegalStateException("doorman-key-backup is null")
|
||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||
Doorman.getInstance().work(key)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (passwordTextField.password.isEmpty()) {
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Doorman.getInstance().work(passwordTextField.password)
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
|
||||
private class MnemonicsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
private val textFields = (1..12).map { PasteTextField(it) }
|
||||
var entropy = byteArrayOf()
|
||||
private set
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
setLocationRelativeTo(null)
|
||||
}
|
||||
|
||||
fun getWords(): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
for (e in textFields) {
|
||||
if (e.text.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
words.add(e.text)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "4dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout).debug(false)
|
||||
val iterator = textFields.iterator()
|
||||
for (i in 1..5 step 2) {
|
||||
for (j in 1..7 step 2) {
|
||||
builder.add(iterator.next()).xy(j, i)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
for (textField in textFields) {
|
||||
if (textField.text.isBlank()) {
|
||||
textField.outline = "error"
|
||||
textField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Mnemonics.MnemonicCode(getWords().joinToString(StringUtils.SPACE)).use {
|
||||
it.validate()
|
||||
entropy = it.toEntropy()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
entropy = byteArrayOf()
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
private inner class PasteTextField(private val index: Int) : OutlineTextField() {
|
||||
init {
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
|
||||
if (text.isEmpty() && index != 1) {
|
||||
textFields[index - 2].requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paste() {
|
||||
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
return
|
||||
}
|
||||
|
||||
val text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
val words = mutableListOf<String>()
|
||||
if (text.count { it == ControlCharacters.SP } > text.count { it == ControlCharacters.LF }) {
|
||||
words.addAll(text.split(StringUtils.SPACE))
|
||||
} else {
|
||||
words.addAll(text.split(ControlCharacters.LF))
|
||||
}
|
||||
val iterator = words.iterator()
|
||||
for (i in index..textFields.size) {
|
||||
if (iterator.hasNext()) {
|
||||
textFields[i - 1].text = iterator.next()
|
||||
textFields[i - 1].requestFocusInWindow()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.database.OwnerType
|
||||
import app.termora.highlight.KeywordHighlightManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.snippet.SnippetManager
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
|
||||
val instance by lazy { MigrationApplicationRunnerExtension() }
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
val file = getDatabaseFile()
|
||||
if (file.exists().not()) return
|
||||
|
||||
// 如果数据库文件存在,那么需要迁移文件
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
try {
|
||||
// 打开数据
|
||||
openDatabase()
|
||||
|
||||
// 尝试解锁
|
||||
openDoor()
|
||||
|
||||
// 询问是否迁移
|
||||
if (askMigrate()) {
|
||||
|
||||
// 迁移
|
||||
migrate()
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
} finally {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
|
||||
}
|
||||
|
||||
private fun openDoor() {
|
||||
if (Doorman.getInstance().isWorking()) {
|
||||
if (DoormanDialog(null).open().not()) {
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDatabase() {
|
||||
try {
|
||||
// 初始化数据库
|
||||
Database.getDatabase()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
JOptionPane.showMessageDialog(
|
||||
null, "Unable to open database",
|
||||
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrate() {
|
||||
val database = Database.getDatabase()
|
||||
val accountManager = AccountManager.getInstance()
|
||||
val databaseManager = DatabaseManager.getInstance()
|
||||
val ownerId = accountManager.getAccountId()
|
||||
val hostManager = HostManager.getInstance()
|
||||
val snippetManager = SnippetManager.getInstance()
|
||||
val macroManager = MacroManager.getInstance()
|
||||
val keymapManager = KeymapManager.getInstance()
|
||||
val keyManager = KeyManager.getInstance()
|
||||
val highlightManager = KeywordHighlightManager.getInstance()
|
||||
val accountOwner = AccountOwner(
|
||||
id = accountManager.getAccountId(),
|
||||
name = accountManager.getEmail(),
|
||||
type = OwnerType.User
|
||||
)
|
||||
|
||||
for (host in database.getHosts()) {
|
||||
if (host.deleted) continue
|
||||
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
|
||||
}
|
||||
|
||||
for (snippet in database.getSnippets()) {
|
||||
if (snippet.deleted) continue
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
|
||||
for (macro in database.getMacros()) {
|
||||
macroManager.addMacro(macro)
|
||||
}
|
||||
|
||||
for (keymap in database.getKeymaps()) {
|
||||
keymapManager.addKeymap(keymap)
|
||||
}
|
||||
|
||||
for (keypair in database.getKeyPairs()) {
|
||||
keyManager.addOhKeyPair(keypair, accountOwner)
|
||||
}
|
||||
|
||||
for (e in database.getKeywordHighlights()) {
|
||||
highlightManager.addKeywordHighlight(e, accountOwner)
|
||||
}
|
||||
|
||||
val list = listOf(
|
||||
database.sync,
|
||||
database.properties,
|
||||
database.terminal,
|
||||
database.sftp,
|
||||
database.appearance,
|
||||
)
|
||||
|
||||
for (e in list) {
|
||||
for (k in e.getProperties()) {
|
||||
databaseManager.setSetting(e.name + "." + k.key, k.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (e in database.safetyProperties.getProperties()) {
|
||||
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun askMigrate(): Boolean {
|
||||
|
||||
if (MigrationDialog(null).open()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun moveOldDirectory() {
|
||||
// 关闭数据库
|
||||
Disposer.dispose(Database.getDatabase())
|
||||
|
||||
val file = getDatabaseFile()
|
||||
FileUtils.moveDirectory(
|
||||
file,
|
||||
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private fun restart() {
|
||||
|
||||
// 重启
|
||||
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
|
||||
|
||||
// 退出程序
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
|
||||
|
||||
fun getDatabaseFile(): File {
|
||||
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
|
||||
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
|
||||
private var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
escapeDispose = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = StringUtils.EMPTY
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
var rows = 2
|
||||
val step = 2
|
||||
val formMargin = "7dlu"
|
||||
val icon = JLabel()
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
|
||||
|
||||
val editorPane = JXEditorPane()
|
||||
editorPane.contentType = "text/html"
|
||||
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
|
||||
editorPane.isEditable = false
|
||||
editorPane.addHyperlinkListener {
|
||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
Application.browse(it.url.toURI())
|
||||
}
|
||||
}
|
||||
editorPane.background = DynamicColor("window")
|
||||
val scrollPane = JScrollPane(editorPane)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
scrollPane.preferredSize = Dimension(Int.MAX_VALUE, 225)
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
|
||||
}
|
||||
})
|
||||
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
isOpened = false
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createOkAction(): AbstractAction {
|
||||
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object MigrationI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(MigrationI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
|
||||
class MigrationPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { MigrationApplicationRunnerExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Migration"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import kotlin.time.measureTime
|
||||
|
||||
object PBKDF2 {
|
||||
|
||||
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||
|
||||
fun generateSecret(
|
||||
password: CharArray,
|
||||
salt: ByteArray,
|
||||
iterationCount: Int = 150000,
|
||||
keyLength: Int = 256
|
||||
): ByteArray {
|
||||
val bytes: ByteArray
|
||||
val time = measureTime {
|
||||
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||
.encoded
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Secret generated $time")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun hash(slat: ByteArray, password: CharArray, iterationCount: Int, keyLength: Int): ByteArray {
|
||||
val spec = PBEKeySpec(password, slat, iterationCount, keyLength)
|
||||
val secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
return secretKeyFactory.generateSecret(spec).encoded
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.AES.encodeBase64String
|
||||
|
||||
class PasswordWrongException : RuntimeException()
|
||||
|
||||
class Doorman private constructor() : Disposable {
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private var key = byteArrayOf()
|
||||
|
||||
companion object {
|
||||
fun getInstance(): Doorman {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(Doorman::class) { Doorman() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isWorking(): Boolean {
|
||||
return properties.getString("doorman", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun encrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.encrypt(key, text.toByteArray()).encodeBase64String()
|
||||
}
|
||||
|
||||
|
||||
fun decrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.decrypt(key, text.decodeBase64()).decodeToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回钥匙
|
||||
*/
|
||||
fun work(password: CharArray): ByteArray {
|
||||
if (key.isNotEmpty()) {
|
||||
throw IllegalStateException("Working")
|
||||
}
|
||||
return work(convertKey(password))
|
||||
}
|
||||
|
||||
fun work(key: ByteArray): ByteArray {
|
||||
val verify = properties.getString("doorman-verify")
|
||||
if (verify == null) {
|
||||
properties.putString(
|
||||
"doorman-verify",
|
||||
AES.ECB.encrypt(key, factor()).encodeBase64String()
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
if (!AES.ECB.decrypt(key, verify.decodeBase64()).contentEquals(factor())) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
}
|
||||
|
||||
this.key = key
|
||||
properties.putString("doorman", "true")
|
||||
|
||||
return this.key
|
||||
}
|
||||
|
||||
|
||||
private fun convertKey(password: CharArray): ByteArray {
|
||||
return PBKDF2.generateSecret(password, factor())
|
||||
}
|
||||
|
||||
|
||||
private fun checkIsWorking() {
|
||||
if (key.isEmpty() || !isWorking()) {
|
||||
throw UnsupportedOperationException("Doorman is not working")
|
||||
}
|
||||
}
|
||||
|
||||
private fun factor(): ByteArray {
|
||||
return Application.getName().toByteArray()
|
||||
}
|
||||
|
||||
fun test(password: CharArray): Boolean {
|
||||
checkIsWorking()
|
||||
return key.contentEquals(convertKey(password))
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
key = byteArrayOf()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.termora.plugins.migration
|
||||
enum class SyncType {
|
||||
GitLab,
|
||||
GitHub,
|
||||
Gitee,
|
||||
WebDAV,
|
||||
}
|
||||
24
plugins/migration/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>migration</id>
|
||||
|
||||
<name>Migration</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<!-- since: >=xxx , or >xxx -->
|
||||
<!-- until: <=xxx , or <xxx -->
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.migration.MigrationPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Migrate version 1.x configuration files to 2.x</description>
|
||||
<description language="zh_CN">将 1.x 版本的配置文件迁移到 2.x</description>
|
||||
<description language="zh_TW">將 1.x 版本的設定檔移轉到 2.x</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
19
plugins/migration/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_659_75852)">
|
||||
<path d="M7.49998 0.523674C8.50002 5.49999 10.5 7.49999 15.554 8.5C10.5 9.49999 8.5 11.5 7.50005 16.4763C6.50002 11.5 4.50002 9.49999 -0.553986 8.49998C4.5 7.49999 6.5 5.49999 7.49998 0.523674Z" fill="url(#paint0_linear_659_75852)"/>
|
||||
<path d="M12.9933 4.90705C14.0451 4.90705 14.8979 4.05433 14.8979 3.00245C14.8979 1.95056 14.0451 1.09784 12.9933 1.09784C11.9414 1.09784 11.0886 1.95056 11.0886 3.00245C11.0886 4.05433 11.9414 4.90705 12.9933 4.90705Z" fill="url(#paint1_linear_659_75852)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3573F0"/>
|
||||
<stop offset="1" stop-color="#EA33EC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3573F0"/>
|
||||
<stop offset="1" stop-color="#EA33EC"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_659_75852">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 is ready.</h1> \
|
||||
<br/> \
|
||||
<h3>1. The storage structure has been updated. Existing data needs to be migrated. Just click <font color="#3573F0">“Migrate”</font> to complete the process.</h3> \
|
||||
<h3>2. The <font color="#3573F0">Sync feature</font> is now provided as a plugin. If needed, please <font color="#EA33EC">manually install</font> it from Settings.</h3> \
|
||||
<h3>3. The <font color="#3573F0">Data Encryption</font> feature has been <font color="#EA33EC">removed</font> (local data will now be stored with basic encryption). Please ensure your device is in a trusted environment.</h3> \
|
||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=Migrate
|
||||
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 已就绪。</h1> \
|
||||
<br/> \
|
||||
<h3>1. 存储结构已更新,需迁移现有数据。只需点击 <font color="#3573F0">“迁移”</font> 即可完成操作。</h3> \
|
||||
<h3>2. <font color="#3573F0">同步功能</font> 现作为插件提供,如需使用,请前往设置中 <font color="#EA33EC">手动安装</font>。</h3> \
|
||||
<h3>3. <font color="#3573F0">数据加密</font> 功能已被 <font color="#EA33EC">移除</font>(本地数据将以简单加密方式存储),请确保你的设备处于可信环境中。</h3> \
|
||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=迁移
|
||||
@@ -0,0 +1,9 @@
|
||||
termora.plugins.migration.message=<html> \
|
||||
<h1 align="center">2.0 已準備就緒。</h1> \
|
||||
<br/> \
|
||||
<h3>1. 儲存結構已更新,需要遷移現有資料。只需點擊 <font color="#3573F0">「遷移」</font> 即可完成操作。</h3> \
|
||||
<h3>2. <font color="#3573F0">同步功能</font> 現以外掛形式提供,如需使用,請至設定中 <font color="#EA33EC">手動安裝</font>。</h3> \
|
||||
<h3>3. <font color="#3573F0">資料加密</font> 功能已被 <font color="#EA33EC">移除</font>(本機資料將以簡易加密方式儲存),請確保你的裝置處於可信環境中。</h3> \
|
||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=遷移
|
||||
16
plugins/obs/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class OBSFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { OBSFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return OBSFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class OBSPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { OBSProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { OBSProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Huawei OBS"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class OBSProtocolHostPanel : ProtocolHostPanel() {
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = OBSProtocolProvider.PROTOCOL
|
||||
)
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { OBSProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return OBSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return OBSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class OBSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { OBSProtocolProvider() }
|
||||
const val PROTOCOL = "OBS"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.huawei
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return OBSFileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class OBSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { OBSProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return OBSProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
24
plugins/obs/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>obs</id>
|
||||
|
||||
<name>Huawei OBS</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.obs.OBSPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to Huawei OBS</description>
|
||||
<description language="zh_CN">支持连接到华为云对象存储</description>
|
||||
<description language="zh_TW">支援連接到華為雲端物件存儲</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/obs/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1747212780529" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M843.5 91.90625H180.59375c-48.65625 0-88.40625 39.75-88.40625 88.40625v662.90625c0 48.65625 39.75 88.40625 88.40625 88.40625h662.90625c48.65625 0 88.40625-39.75 88.40625-88.40625V180.21875c0-48.5625-39.75-88.3125-88.40625-88.3125z m-441.5625 636.375c-16.5 11.15625-76.21875 49.6875-114.46875 32.53125-38.34375-17.15625-58.875-60-58.875-60l217.03125-5.8125s-28.3125 21.5625-43.6875 33.28125z m-125.15625-49.78125c-22.3125-3.375-71.0625-16.21875-96-58.125-25.03125-41.90625-12.9375-97.5-9.84375-98.34375 3.09375-0.84375 83.0625 43.78125 121.875 63.28125 38.90625 19.40625 154.875 89.625 155.71875 92.15625 1.125 3-146.625 4.59375-171.75 1.03125z m5.53125-126.9375c-52.875-35.25-71.90625-64.6875-67.78125-108.9375 4.125-44.25 49.5-89.53125 55.875-90.28125 6.28125-0.65625 60.9375 70.40625 103.03125 136.78125 42 66.375 93.46875 157.6875 91.125 163.6875-2.34375 6-129-63.46875-182.25-101.25z m212.34375 80.34375c-0.1875 2.8125-4.125 4.125-7.3125 1.03125-3.28125-3.09375-91.59375-144.1875-108.375-180.09375-16.78125-35.90625-43.3125-105.65625-7.96875-151.21875 35.34375-45.65625 90.65625-43.125 90.65625-43.125 3.5625 5.4375 27.09375 68.8125 35.0625 118.6875 8.0625 49.6875-1.875 251.90625-2.0625 254.71875z m34.78125 0c-0.1875-2.8125-10.125-204.9375-2.15625-254.8125 7.96875-49.875 31.5-113.15625 35.0625-118.6875 0 0 55.3125-2.53125 90.65625 43.125s8.8125 115.3125-7.96875 151.21875C628.25 488.65625 539.9375 629.75 536.65625 632.84375c-3.09375 3.09375-7.03125 1.78125-7.21875-0.9375z m30.1875 21c-2.34375-6 49.03125-97.21875 91.125-163.6875 42-66.375 96.65625-137.53125 103.03125-136.78125 6.28125 0.65625 51.75 45.9375 55.875 90.28125 4.125 44.25-14.90625 73.78125-67.78125 108.9375-53.34375 37.6875-179.90625 107.15625-182.25 101.25z m177.09375 107.90625c-38.34375 17.15625-98.0625-21.375-114.5625-32.53125-15.375-11.71875-43.6875-33.28125-43.6875-33.28125l217.03125 5.8125c0.09375-0.09375-20.53125 42.84375-58.78125 60z m106.59375-140.4375c-25.03125 41.90625-73.6875 54.65625-96 58.125-25.125 3.5625-172.78125 1.96875-171.75-1.03125 0.84375-2.625 116.90625-72.75 155.71875-92.15625 38.8125-19.40625 118.78125-64.125 121.875-63.28125 3.09375 0.9375 15.1875 56.53125-9.84375 98.34375z" fill="#6C707E" p-id="1351"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1747212780529" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M843.5 91.90625H180.59375c-48.65625 0-88.40625 39.75-88.40625 88.40625v662.90625c0 48.65625 39.75 88.40625 88.40625 88.40625h662.90625c48.65625 0 88.40625-39.75 88.40625-88.40625V180.21875c0-48.5625-39.75-88.3125-88.40625-88.3125z m-441.5625 636.375c-16.5 11.15625-76.21875 49.6875-114.46875 32.53125-38.34375-17.15625-58.875-60-58.875-60l217.03125-5.8125s-28.3125 21.5625-43.6875 33.28125z m-125.15625-49.78125c-22.3125-3.375-71.0625-16.21875-96-58.125-25.03125-41.90625-12.9375-97.5-9.84375-98.34375 3.09375-0.84375 83.0625 43.78125 121.875 63.28125 38.90625 19.40625 154.875 89.625 155.71875 92.15625 1.125 3-146.625 4.59375-171.75 1.03125z m5.53125-126.9375c-52.875-35.25-71.90625-64.6875-67.78125-108.9375 4.125-44.25 49.5-89.53125 55.875-90.28125 6.28125-0.65625 60.9375 70.40625 103.03125 136.78125 42 66.375 93.46875 157.6875 91.125 163.6875-2.34375 6-129-63.46875-182.25-101.25z m212.34375 80.34375c-0.1875 2.8125-4.125 4.125-7.3125 1.03125-3.28125-3.09375-91.59375-144.1875-108.375-180.09375-16.78125-35.90625-43.3125-105.65625-7.96875-151.21875 35.34375-45.65625 90.65625-43.125 90.65625-43.125 3.5625 5.4375 27.09375 68.8125 35.0625 118.6875 8.0625 49.6875-1.875 251.90625-2.0625 254.71875z m34.78125 0c-0.1875-2.8125-10.125-204.9375-2.15625-254.8125 7.96875-49.875 31.5-113.15625 35.0625-118.6875 0 0 55.3125-2.53125 90.65625 43.125s8.8125 115.3125-7.96875 151.21875C628.25 488.65625 539.9375 629.75 536.65625 632.84375c-3.09375 3.09375-7.03125 1.78125-7.21875-0.9375z m30.1875 21c-2.34375-6 49.03125-97.21875 91.125-163.6875 42-66.375 96.65625-137.53125 103.03125-136.78125 6.28125 0.65625 51.75 45.9375 55.875 90.28125 4.125 44.25-14.90625 73.78125-67.78125 108.9375-53.34375 37.6875-179.90625 107.15625-182.25 101.25z m177.09375 107.90625c-38.34375 17.15625-98.0625-21.375-114.5625-32.53125-15.375-11.71875-43.6875-33.28125-43.6875-33.28125l217.03125 5.8125c0.09375-0.09375-20.53125 42.84375-58.78125 60z m106.59375-140.4375c-25.03125 41.90625-73.6875 54.65625-96 58.125-25.125 3.5625-172.78125 1.96875-171.75-1.03125 0.84375-2.625 116.90625-72.75 155.71875-92.15625 38.8125-19.40625 118.78125-64.125 121.875-63.28125 3.09375 0.9375 15.1875 56.53125-9.84375 98.34375z" fill="#CED0D6" p-id="1351"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
17
plugins/oss/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||
implementation("javax.activation:activation:1.1.1")
|
||||
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class OSSFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { OSSFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return OSSFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class OSSPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { OSSProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { OSSProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Alibaba OSS"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class OSSProtocolHostPanel : ProtocolHostPanel() {
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = OSSProtocolProvider.PROTOCOL
|
||||
)
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { OSSProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return OSSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return OSSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class OSSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { OSSProtocolProvider() }
|
||||
const val PROTOCOL = "OSS"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.aliyun
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return OSSFileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class OSSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { OSSProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return OSSProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
24
plugins/oss/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>oss</id>
|
||||
|
||||
<name>Alibaba OSS</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.oss.OSSPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to Alibaba OSS</description>
|
||||
<description language="zh_CN">支持连接到阿里云对象存储</description>
|
||||
<description language="zh_TW">支援連接到阿里雲物件存儲</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/oss/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1747212946112" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M853.33333333 79.6444448H170.66666667C120.6044448 79.6444448 79.6444448 120.6044448 79.6444448 170.66666667v682.66666666c0 50.06222187 40.96 91.02222187 91.02222187 91.02222187h682.66666666c50.06222187 0 91.02222187-40.96 91.02222187-91.02222187V170.66666667c0-50.06222187-40.96-91.02222187-91.02222187-91.02222187zM298.09777813 700.87111147c-56.88888853 0-104.6755552-45.51111147-104.67555626-102.4v-175.21777814c0-56.88888853 45.51111147-102.4 104.67555626-102.4v-2.2755552H466.48888853l-13.65333333 59.16444374-145.6355552 31.85777813c-13.65333333 4.55111147-22.7555552 13.65333333-22.7555552 27.30666667v147.91111146c0 13.65333333 11.37777813 25.03111147 22.7555552 27.30666667l143.36 29.58222187 13.65333333 59.1644448h-166.1155552z m273.06666667-202.5244448v29.58222186h-116.05333333v-29.58222186h116.05333333zM830.57777813 600.74666667c0 56.88888853-45.51111147 102.4-104.67555626 102.4H557.51111147l13.65333333-59.1644448 143.36-29.58222187c13.65333333-4.55111147 22.7555552-13.65333333 22.7555552-27.30666667v-147.91111146c0-13.65333333-11.37777813-25.03111147-22.7555552-27.30666667l-143.36-29.58222187-13.65333333-59.1644448h166.1155552c56.88888853 0 104.6755552 45.51111147 104.6755552 102.4v175.21777814z" p-id="1351" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1747211795611" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1290" width="16" height="16"><path d="M832 106.666667H192C145.066667 106.666667 106.666667 145.066667 106.666667 192v640c0 46.933333 38.4 85.333333 85.333333 85.333333h640c46.933333 0 85.333333-38.4 85.333333-85.333333V192c0-46.933333-38.4-85.333333-85.333333-85.333333zM311.466667 689.066667c-53.333333 0-98.133333-42.666667-98.133334-96v-164.266667c0-53.333333 42.666667-96 98.133334-96v-2.133333H469.333333l-12.8 55.466666-136.533333 29.866667c-12.8 4.266667-21.333333 12.8-21.333333 25.6v138.666667c0 12.8 10.666667 23.466667 21.333333 25.6l134.4 27.733333 12.8 55.466667h-155.733333z m256-189.866667v27.733333h-108.8v-27.733333h108.8zM810.666667 595.2c0 53.333333-42.666667 96-98.133334 96H554.666667l12.8-55.466667 134.4-27.733333c12.8-4.266667 21.333333-12.8 21.333333-25.6v-138.666667c0-12.8-10.666667-23.466667-21.333333-25.6l-134.4-27.733333-12.8-55.466667h155.733333c53.333333 0 98.133333 42.666667 98.133333 96v164.266667z" p-id="1291" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
21
plugins/s3/build.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(platform(libs.testcontainers.bom))
|
||||
testImplementation(libs.testcontainers)
|
||||
testImplementation(libs.testcontainers.junit.jupiter)
|
||||
testImplementation(project(":"))
|
||||
|
||||
implementation("io.minio:minio:8.5.17")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,223 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.vfs2.FileObjectDescriptor
|
||||
import io.minio.*
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.FileType
|
||||
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||
import org.apache.commons.vfs2.provider.AbstractFileObject
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
|
||||
class S3FileObject(
|
||||
private val minio: MinioClient,
|
||||
fileName: AbstractFileName,
|
||||
fileSystem: S3FileSystem
|
||||
) : AbstractFileObject<S3FileSystem>(fileName, fileSystem), FileObjectDescriptor {
|
||||
private var attributes = Attributes()
|
||||
|
||||
init {
|
||||
attributes = attributes.copy(isRoot = name.path == fileSystem.getDelimiter())
|
||||
}
|
||||
|
||||
override fun doGetContentSize(): Long {
|
||||
return attributes.size
|
||||
}
|
||||
|
||||
override fun doGetType(): FileType {
|
||||
return if (attributes.isRoot || attributes.isBucket) FileType.FOLDER
|
||||
else if (attributes.isDirectory && attributes.isFile) FileType.FILE_OR_FOLDER
|
||||
else if (attributes.isFile) FileType.FILE
|
||||
else if (attributes.isDirectory) FileType.FOLDER
|
||||
else FileType.IMAGINARY
|
||||
}
|
||||
|
||||
override fun doListChildren(): Array<out String?>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun doCreateFolder() {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
private fun getBucketName(): String {
|
||||
if (StringUtils.isNotBlank(attributes.bucket)) {
|
||||
return attributes.bucket
|
||||
}
|
||||
if (parent is S3FileObject) {
|
||||
return (parent as S3FileObject).getBucketName()
|
||||
}
|
||||
throw IllegalArgumentException("Bucket must be a S3 file object")
|
||||
}
|
||||
|
||||
override fun doListChildrenResolved(): Array<FileObject>? {
|
||||
if (isFile) return null
|
||||
|
||||
val children = mutableListOf<FileObject>()
|
||||
|
||||
if (attributes.isRoot) {
|
||||
val buckets = minio.listBuckets()
|
||||
for (bucket in buckets) {
|
||||
val file = resolveFile(bucket.name())
|
||||
if (file is S3FileObject) {
|
||||
file.attributes = file.attributes.copy(
|
||||
isBucket = true,
|
||||
bucket = bucket.name(),
|
||||
isDirectory = false,
|
||||
isFile = false,
|
||||
lastModified = bucket.creationDate().toInstant().toEpochMilli()
|
||||
)
|
||||
children.add(file)
|
||||
}
|
||||
}
|
||||
} else if (attributes.isBucket || attributes.isDirectory) {
|
||||
val builder = ListObjectsArgs.builder().bucket(getBucketName())
|
||||
.delimiter(fileSystem.getDelimiter())
|
||||
var prefix = StringUtils.EMPTY
|
||||
if (attributes.isDirectory) {
|
||||
// remove first delimiter
|
||||
prefix = StringUtils.removeStart(name.path, fileSystem.getDelimiter())
|
||||
// remove bucket
|
||||
prefix = StringUtils.removeStart(prefix, getBucketName())
|
||||
// remove first delimiter
|
||||
prefix = StringUtils.removeStart(prefix, fileSystem.getDelimiter())
|
||||
// remove last delimiter
|
||||
prefix = StringUtils.removeEnd(prefix, fileSystem.getDelimiter())
|
||||
prefix = prefix + fileSystem.getDelimiter()
|
||||
}
|
||||
builder.prefix(prefix)
|
||||
|
||||
for (e in minio.listObjects(builder.build())) {
|
||||
val item = e.get()
|
||||
val objectName = StringUtils.removeStart(item.objectName(), prefix)
|
||||
val file = resolveFile(objectName)
|
||||
if (file is S3FileObject) {
|
||||
val lastModified = if (item.lastModified() != null) item.lastModified()
|
||||
.toInstant().toEpochMilli() else 0
|
||||
val owner = if (item.owner() != null) item.owner().displayName() else StringUtils.EMPTY
|
||||
file.attributes = file.attributes.copy(
|
||||
bucket = attributes.bucket,
|
||||
isDirectory = item.isDir,
|
||||
isFile = item.isDir.not(),
|
||||
lastModified = lastModified,
|
||||
size = if (item.isDir.not()) item.size() else 0,
|
||||
owner = owner
|
||||
)
|
||||
children.add(file)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return children.toTypedArray()
|
||||
}
|
||||
|
||||
override fun getFileSystem(): S3FileSystem {
|
||||
return super.getFileSystem() as S3FileSystem
|
||||
}
|
||||
|
||||
override fun doGetLastModifiedTime(): Long {
|
||||
return attributes.lastModified
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon? {
|
||||
if (attributes.isBucket) {
|
||||
return Icons.dbms
|
||||
}
|
||||
return super.getIcon(width, height)
|
||||
}
|
||||
|
||||
override fun getTypeDescription(): String? {
|
||||
if (attributes.isBucket) {
|
||||
return "Bucket"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getLastModified(): Long? {
|
||||
return attributes.lastModified
|
||||
}
|
||||
|
||||
override fun getOwner(): String? {
|
||||
return attributes.owner
|
||||
}
|
||||
|
||||
override fun doDelete() {
|
||||
if (isFile) {
|
||||
minio.removeObject(
|
||||
RemoveObjectArgs.builder()
|
||||
.bucket(getBucketName()).`object`(getObjectName()).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doGetOutputStream(bAppend: Boolean): OutputStream? {
|
||||
return createStreamer()
|
||||
}
|
||||
|
||||
private fun createStreamer(): OutputStream {
|
||||
val pis = PipedInputStream()
|
||||
val pos = PipedOutputStream(pis)
|
||||
|
||||
val thread = Thread.ofVirtual().start {
|
||||
minio.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(getBucketName())
|
||||
.stream(pis, -1, 32 * 1024 * 1024)
|
||||
.`object`(getObjectName()).build()
|
||||
)
|
||||
IOUtils.closeQuietly(pis)
|
||||
}
|
||||
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
pos.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
pos.close()
|
||||
thread.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun doGetInputStream(bufferSize: Int): InputStream? {
|
||||
return minio.getObject(GetObjectArgs.builder().bucket(getBucketName()).`object`(getObjectName()).build())
|
||||
}
|
||||
|
||||
private fun getObjectName(): String {
|
||||
var objectName = StringUtils.removeStart(name.path, fileSystem.getDelimiter())
|
||||
objectName = StringUtils.removeStart(objectName, getBucketName())
|
||||
objectName = StringUtils.removeStart(objectName, fileSystem.getDelimiter())
|
||||
return objectName
|
||||
}
|
||||
|
||||
private data class Attributes(
|
||||
val isRoot: Boolean = false,
|
||||
val isBucket: Boolean = false,
|
||||
val isDirectory: Boolean = false,
|
||||
val isFile: Boolean = false,
|
||||
/**
|
||||
* 只要不是 root 那么一定存在 bucket
|
||||
*/
|
||||
val bucket: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 最后修改时间
|
||||
*/
|
||||
val lastModified: Long = 0,
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
val size: Long = 0,
|
||||
/**
|
||||
* 所有者
|
||||
*/
|
||||
val owner: String = StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import io.minio.MinioClient
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class S3FileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { S3FileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return S3FileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
options: FileSystemOptions
|
||||
): FileSystem {
|
||||
val region = S3FileSystemConfigBuilder.instance.getRegion(options)
|
||||
val endpoint = S3FileSystemConfigBuilder.instance.getEndpoint(options)
|
||||
val accessKey = S3FileSystemConfigBuilder.instance.getAccessKey(options)
|
||||
val secretKey = S3FileSystemConfigBuilder.instance.getSecretKey(options)
|
||||
val builder = MinioClient.builder()
|
||||
builder.endpoint(endpoint)
|
||||
builder.credentials(accessKey, secretKey)
|
||||
if (region.isNotBlank()) builder.region(region)
|
||||
return S3FileSystem(builder.build(), rootFileName, options)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import io.minio.MinioClient
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||
import org.apache.commons.vfs2.provider.AbstractFileSystem
|
||||
|
||||
class S3FileSystem(
|
||||
private val minio: MinioClient,
|
||||
rootName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
) : AbstractFileSystem(rootName, null, fileSystemOptions) {
|
||||
|
||||
override fun addCapabilities(caps: MutableCollection<Capability>) {
|
||||
caps.addAll(S3FileProvider.capabilities)
|
||||
}
|
||||
|
||||
override fun createFile(name: AbstractFileName): FileObject? {
|
||||
return S3FileObject(minio, name, this)
|
||||
}
|
||||
|
||||
fun getDelimiter(): String {
|
||||
return S3FileSystemConfigBuilder.instance.getDelimiter(fileSystemOptions)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
minio.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.vfs2.s3.AbstractS3FileSystemConfigBuilder
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
|
||||
class S3FileSystemConfigBuilder private constructor() : AbstractS3FileSystemConfigBuilder() {
|
||||
companion object {
|
||||
val instance by lazy { S3FileSystemConfigBuilder() }
|
||||
}
|
||||
|
||||
override fun getConfigClass(): Class<out FileSystem> {
|
||||
return S3FileSystem::class.java
|
||||
}
|
||||
}
|
||||