chore!: migrate to version 2.x

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

10
plugins/LICENSE Normal file
View 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
View 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/

View 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")

View File

@@ -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)
}
}

View File

@@ -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
}
}

View 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
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View 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>

View 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

View 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="#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

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.interval=Interval
termora.plugins.bg.background-image=Background Image

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔

89
plugins/common.gradle.kts Normal file
View 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()
}
}

View 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")

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}

View File

@@ -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
}
}

View 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>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View 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")

View File

@@ -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
}
}

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View 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>

View 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">
<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

View 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">
<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

View File

@@ -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();
}
});
}
}

View 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")

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}

View File

@@ -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
}
}

View 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>

View 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

View 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="#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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 453 B

View File

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

After

Width:  |  Height:  |  Size: 453 B

View File

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

View File

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

View File

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

View 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")

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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")
}
}

View File

@@ -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"))
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,7 @@
package app.termora.plugins.migration
enum class SyncType {
GitLab,
GitHub,
Gitee,
WebDAV,
}

View 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>

View 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

View File

@@ -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

View File

@@ -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=迁移

View File

@@ -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=遷移

View 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")

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}

View File

@@ -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
}
}

View 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>

View 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

View 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="#CED0D6" p-id="1351"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View 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")

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}

View File

@@ -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
}
}

View 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>

View 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

View File

@@ -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

View 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")

View File

@@ -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
)
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More