mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
17 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4364bcd6a | ||
|
|
d0827c3b0c | ||
|
|
036a04b0b3 | ||
|
|
eee016c643 | ||
|
|
472bf6e81f | ||
|
|
21229e352f | ||
|
|
1138f48a6e | ||
|
|
f044e0480e | ||
|
|
7047f17783 | ||
|
|
9308f15abb | ||
|
|
b2672f11fc | ||
|
|
f92c6586b2 | ||
|
|
69e07a9bd9 | ||
|
|
cdec60fd25 | ||
|
|
7c30933794 | ||
|
|
b892d2fe13 | ||
|
|
91ee463d41 |
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
|||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.8.0"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.21.2"
|
testcontainers = "1.21.3"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.0"
|
jSerialComm = "2.11.0"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
@@ -43,9 +43,9 @@ restart4j = "0.0.1"
|
|||||||
eddsa = "0.3.0"
|
eddsa = "0.3.0"
|
||||||
exposed = "1.0.0-beta-2"
|
exposed = "1.0.0-beta-2"
|
||||||
h2 = "2.3.232"
|
h2 = "2.3.232"
|
||||||
sqlite = "3.50.1.0"
|
sqlite = "3.50.2.0"
|
||||||
jug = "5.1.0"
|
jug = "5.1.0"
|
||||||
semver4j = "5.8.0"
|
semver4j = "6.0.0"
|
||||||
jsvg = "1.4.0"
|
jsvg = "1.4.0"
|
||||||
dom4j = "2.1.4"
|
dom4j = "2.1.4"
|
||||||
|
|
||||||
|
|||||||
@@ -73,3 +73,7 @@ https://www.apache.org/licenses/LICENSE-2.0.html
|
|||||||
GeoLite2 (https://www.maxmind.com)
|
GeoLite2 (https://www.maxmind.com)
|
||||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
https://creativecommons.org/licenses/by-sa/4.0/
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
smbj
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER
|
||||||
@@ -8,7 +8,7 @@ project.version = "0.0.2"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.qcloud:cos_api:5.6.245")
|
implementation("com.qcloud:cos_api:5.6.247")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ project.version = "0.0.1"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
|
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
plugins/smb/build.gradle.kts
Normal file
14
plugins/smb/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.1"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
implementation("com.hierynomus:smbj:0.14.0")
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
|
||||||
|
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||||
|
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
share.close()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import com.hierynomus.msdtyp.AccessMask
|
||||||
|
import com.hierynomus.msfscc.FileAttributes
|
||||||
|
import com.hierynomus.mssmb2.SMB2CreateDisposition
|
||||||
|
import com.hierynomus.mssmb2.SMB2CreateOptions
|
||||||
|
import com.hierynomus.mssmb2.SMB2ShareAccess
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
|
|
||||||
|
class SMBFileSystemProvider(private val share: DiskShare, private val session: Session) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "smb"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
val file = share.openFile(
|
||||||
|
path.absolutePathString(),
|
||||||
|
setOf(AccessMask.GENERIC_WRITE),
|
||||||
|
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||||
|
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||||
|
SMB2CreateDisposition.FILE_OVERWRITE_IF,
|
||||||
|
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||||
|
)
|
||||||
|
val os = file.outputStream
|
||||||
|
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
os.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(os)
|
||||||
|
file.closeNoWait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val file = share.openFile(
|
||||||
|
path.absolutePathString(),
|
||||||
|
setOf(AccessMask.GENERIC_READ),
|
||||||
|
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||||
|
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||||
|
SMB2CreateDisposition.FILE_OPEN,
|
||||||
|
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||||
|
)
|
||||||
|
val input = file.inputStream
|
||||||
|
return object : InputStream() {
|
||||||
|
override fun read(): Int = input.read()
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(input)
|
||||||
|
file.closeNoWait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
val absolutePath = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||||
|
for (information in share.list(if (absolutePath == path.fileSystem.separator) StringUtils.EMPTY else absolutePath)) {
|
||||||
|
if (information.fileName == "." || information.fileName == "..") continue
|
||||||
|
val isDir = information.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L
|
||||||
|
val path = path.resolve(information.fileName)
|
||||||
|
path.attributes = path.attributes.copy(
|
||||||
|
directory = isDir, regularFile = isDir.not(),
|
||||||
|
size = information.endOfFile,
|
||||||
|
lastModifiedTime = information.lastWriteTime.toDate().time,
|
||||||
|
lastAccessTime = information.lastAccessTime.toDate().time,
|
||||||
|
)
|
||||||
|
paths.add(path)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||||
|
share.mkdir(dir.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
if (isDirectory) {
|
||||||
|
share.rmdir(path.absolutePathString(), false)
|
||||||
|
} else {
|
||||||
|
share.rm(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
if (share.fileExists(path.absolutePathString()) || share.folderExists(path.absolutePathString())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class SMBHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = SMBProtocolProvider.PROTOCOL
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = generalOption.portTextField.value as Int
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"smb.share" to generalOption.shareTextField.text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.selectedItem as String,
|
||||||
|
authentication = authentication,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.selectedItem = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
|| validateField(generalOption.shareTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val username = generalOption.usernameTextField.selectedItem as String?
|
||||||
|
if (username.isNullOrBlank()) {
|
||||||
|
setOutlineError(generalOption.usernameTextField)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JComponent) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(SMBClient.DEFAULT_PORT)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val shareTextField = OutlineTextField(256)
|
||||||
|
val usernameTextField = OutlineComboBox<String>()
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
usernameTextField.isEditable = true
|
||||||
|
usernameTextField.addItem("Guest")
|
||||||
|
usernameTextField.addItem("Anonymous")
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${SMBI18n.getString("termora.plugins.smb.share")}:").xy(1, rows)
|
||||||
|
.add(shareTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object SMBI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(SMBI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String {
|
||||||
|
return try {
|
||||||
|
substitutor.replace(getBundle().getString(key))
|
||||||
|
} catch (_: MissingResourceException) {
|
||||||
|
I18n.getString(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class SMBPathHandler(
|
||||||
|
private val client: SMBClient,
|
||||||
|
private val session: Session,
|
||||||
|
fileSystem: FileSystem, path: Path
|
||||||
|
) : PathHandler(fileSystem, path) {
|
||||||
|
override fun dispose() {
|
||||||
|
super.dispose()
|
||||||
|
session.close()
|
||||||
|
IOUtils.closeQuietly(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
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 SMBPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { SMBProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SMBProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "SMB"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class SMBProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = SMBHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return SMBProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||||
|
return SMBProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
import com.hierynomus.smbj.auth.AuthenticationContext
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
class SMBProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolProvider() }
|
||||||
|
const val PROTOCOL = "SMB"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.windows7
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val client = SMBClient()
|
||||||
|
val host = requester.host
|
||||||
|
val connection = client.connect(host.host, host.port)
|
||||||
|
val session = when (host.username) {
|
||||||
|
"Guest" -> connection.authenticate(AuthenticationContext.guest())
|
||||||
|
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
|
||||||
|
else -> connection.authenticate(
|
||||||
|
AuthenticationContext(
|
||||||
|
host.username,
|
||||||
|
host.authentication.password.toCharArray(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val share = session.connectShare(host.options.extras["smb.share"] ?: StringUtils.EMPTY) as DiskShare
|
||||||
|
var sftpDefaultDirectory = StringUtils.defaultString(host.options.sftpDefaultDirectory)
|
||||||
|
sftpDefaultDirectory = if (sftpDefaultDirectory.isNotBlank()) {
|
||||||
|
FilenameUtils.separatorsToUnix(sftpDefaultDirectory)
|
||||||
|
} else {
|
||||||
|
"/"
|
||||||
|
}
|
||||||
|
|
||||||
|
val fs = SMBFileSystem(share, session)
|
||||||
|
return SMBPathHandler(client, session, fs, fs.getPath(sftpDefaultDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class SMBProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return SMBProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/smb/src/main/resources/META-INF/plugin.xml
Normal file
24
plugins/smb/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>smb</id>
|
||||||
|
|
||||||
|
<name>SMB</name>
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.smb.SMBPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to SMB</description>
|
||||||
|
<description language="zh_CN">支持连接到 SMB</description>
|
||||||
|
<description language="zh_TW">支援連接到 SMB</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/smb/src/main/resources/META-INF/pluginIcon.svg
Normal file
1
plugins/smb/src/main/resources/META-INF/pluginIcon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
1
plugins/smb/src/main/resources/i18n/messages.properties
Normal file
1
plugins/smb/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=Share name
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=共享名称
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=共享名稱
|
||||||
@@ -14,3 +14,4 @@ include("plugins:migration")
|
|||||||
include("plugins:editor")
|
include("plugins:editor")
|
||||||
include("plugins:geo")
|
include("plugins:geo")
|
||||||
include("plugins:webdav")
|
include("plugins:webdav")
|
||||||
|
include("plugins:smb")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package app.termora
|
|||||||
|
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
|
||||||
open class DynamicIcon(name: String, private val darkName: String, val allowColorFilter: Boolean = true) :
|
open class DynamicIcon(name: String, private val darkName: String = name, val allowColorFilter: Boolean = true) :
|
||||||
FlatSVGIcon(name) {
|
FlatSVGIcon(name) {
|
||||||
constructor(name: String) : this(name, name)
|
constructor(name: String) : this(name, name)
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,27 @@ data class SerialComm(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HostTag(val text: String)
|
data class LoginScript(
|
||||||
|
/**
|
||||||
|
* 等待字符串
|
||||||
|
*/
|
||||||
|
val expect: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待之后发送
|
||||||
|
*/
|
||||||
|
val send: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [expect] 是否是正则
|
||||||
|
*/
|
||||||
|
val regex: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [expect] 是否大小写匹配,如果为 true 表示不忽略大小写,也就是:'A != a';如果为 false 那么 'A == a'
|
||||||
|
*/
|
||||||
|
val matchCase: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -97,6 +117,10 @@ data class Options(
|
|||||||
* 跳板机
|
* 跳板机
|
||||||
*/
|
*/
|
||||||
val jumpHosts: List<String> = mutableListOf(),
|
val jumpHosts: List<String> = mutableListOf(),
|
||||||
|
/**
|
||||||
|
* 登录脚本
|
||||||
|
*/
|
||||||
|
val loginScripts: List<LoginScript> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* 编码
|
* 编码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class HostManager private constructor() : Disposable {
|
|||||||
fun getHost(id: String): Host? {
|
fun getHost(id: String): Host? {
|
||||||
val data = databaseManager.data(id) ?: return null
|
val data = databaseManager.data(id) ?: return null
|
||||||
if (data.type != DataType.Host.name) return null
|
if (data.type != DataType.Host.name) return null
|
||||||
|
if (data.deleted) return null
|
||||||
return ohMyJson.decodeFromString(data.data)
|
return ohMyJson.decodeFromString(data.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ object Icons {
|
|||||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||||
|
val windows7 by lazy { DynamicIcon("icons/windows7.svg", allowColorFilter = false) }
|
||||||
val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") }
|
val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") }
|
||||||
val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
|
val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
|
||||||
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 开启 PTY
|
// 开启 PTY
|
||||||
val ptyConnector = openPtyConnector()
|
val ptyConnector = loginScriptsPtyConnector(host, openPtyConnector())
|
||||||
ptyConnectorDelegate.ptyConnector = ptyConnector
|
ptyConnectorDelegate.ptyConnector = ptyConnector
|
||||||
|
|
||||||
// 开启 reader
|
// 开启 reader
|
||||||
@@ -81,6 +81,73 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录脚本
|
||||||
|
*/
|
||||||
|
open fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
val loginScripts = host.options.loginScripts.toMutableList()
|
||||||
|
if (loginScripts.isEmpty()) {
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : PtyConnectorDelegate(ptyConnector) {
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
val len = super.read(buffer)
|
||||||
|
|
||||||
|
// 获取一个匹配的登录脚本
|
||||||
|
val scripts = runCatching { popLoginScript(buffer, len) }.getOrNull() ?: return len
|
||||||
|
if (scripts.isEmpty()) return len
|
||||||
|
|
||||||
|
for (script in scripts) {
|
||||||
|
// send
|
||||||
|
write(script.send.toByteArray(getCharset()))
|
||||||
|
|
||||||
|
// send \r or \n
|
||||||
|
val enter = terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(getCharset())
|
||||||
|
write(enter)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popLoginScript(buffer: CharArray, len: Int): List<LoginScript> {
|
||||||
|
if (loginScripts.isEmpty()) return emptyList()
|
||||||
|
if (len < 1) return emptyList()
|
||||||
|
|
||||||
|
val scripts = mutableListOf<LoginScript>()
|
||||||
|
val text = String(buffer, 0, len)
|
||||||
|
val iterator = loginScripts.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val script = iterator.next()
|
||||||
|
if (script.expect.isEmpty()) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
} else if (script.regex) {
|
||||||
|
val regex = if (script.matchCase) script.expect.toRegex()
|
||||||
|
else script.expect.toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
if (regex.matches(text)) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (text.contains(script.expect, script.matchCase.not())) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
||||||
ptyConnector.write(bytes)
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
|
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
|
||||||
setLocationRelativeTo(owner)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import app.termora.Application.ohMyJson
|
|||||||
import app.termora.account.Account
|
import app.termora.account.Account
|
||||||
import app.termora.account.AccountExtension
|
import app.termora.account.AccountExtension
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.database.Data.Companion.toData
|
import app.termora.database.Data.Companion.toData
|
||||||
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
|
import app.termora.snippet.SnippetManager
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.inList
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.statements.StatementType
|
import org.jetbrains.exposed.v1.core.statements.StatementType
|
||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
@@ -402,41 +407,65 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun silentDelete(id: String) {
|
||||||
|
lock.withLock {
|
||||||
|
transaction(database) {
|
||||||
|
DataEntity.deleteWhere { DataEntity.id.eq(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun transferData(account: Account) {
|
private fun transferData(account: Account) {
|
||||||
val deleteIds = mutableSetOf<String>()
|
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 = account.id,
|
||||||
|
name = account.email,
|
||||||
|
type = OwnerType.User
|
||||||
|
)
|
||||||
|
|
||||||
for (host in hostManager.hosts()) {
|
for (host in hostManager.hosts()) {
|
||||||
|
// 已经删除,则忽略
|
||||||
|
if (host.deleted) continue
|
||||||
// 不是用户数据,那么忽略
|
// 不是用户数据,那么忽略
|
||||||
if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue
|
if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue
|
||||||
// 不是本地用户数据,那么忽略
|
// 不是本地用户数据,那么忽略
|
||||||
if (AccountManager.isLocally(host.ownerId).not()) continue
|
if (AccountManager.isLocally(host.ownerId).not()) continue
|
||||||
// 转移资产
|
// 先删除,因为 ID 没有改变,改变的只是 owner 信息
|
||||||
val newHost = host.copy(
|
silentDelete(host.id)
|
||||||
id = randomUUID(),
|
hostManager.addHost(host.copy(ownerId = accountOwner.id, ownerType = accountOwner.type.name))
|
||||||
ownerId = account.id,
|
|
||||||
ownerType = OwnerType.User.name,
|
|
||||||
)
|
|
||||||
// 保存数据
|
|
||||||
save(
|
|
||||||
Data(
|
|
||||||
id = newHost.id,
|
|
||||||
ownerId = newHost.ownerId,
|
|
||||||
ownerType = newHost.ownerType,
|
|
||||||
type = DataType.Host.name,
|
|
||||||
data = ohMyJson.encodeToString(newHost),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
deleteIds.add(host.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteIds.isNotEmpty()) {
|
for (snippet in snippetManager.snippets()) {
|
||||||
lock.withLock {
|
if (snippet.deleted) continue
|
||||||
transaction(database) {
|
silentDelete(snippet.id)
|
||||||
DataEntity.deleteWhere { DataEntity.id.inList(deleteIds) }
|
snippetManager.addSnippet(snippet)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (macro in macroManager.getMacros()) {
|
||||||
|
silentDelete(macro.id)
|
||||||
|
macroManager.addMacro(macro)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keymap in keymapManager.getKeymaps()) {
|
||||||
|
silentDelete(keymap.id)
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keypair in keyManager.getOhKeyPairs()) {
|
||||||
|
silentDelete(keypair.id)
|
||||||
|
keyManager.addOhKeyPair(keypair, accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in highlightManager.getKeywordHighlights()) {
|
||||||
|
silentDelete(e.id)
|
||||||
|
highlightManager.addKeywordHighlight(e, accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ordered(): Long {
|
override fun ordered(): Long {
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class NewKeywordHighlightDialog(
|
|||||||
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
|
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
|
||||||
I18n.getString("termora.highlight.background-color")
|
I18n.getString("termora.highlight.background-color")
|
||||||
)
|
)
|
||||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
val matchCaseBtn = JToggleButton(Icons.matchCase).apply { toolTipText = I18n.getString("termora.match-case") }
|
||||||
val regexBtn = JToggleButton(Icons.regex)
|
val regexBtn = JToggleButton(Icons.regex).apply { toolTipText = I18n.getString("termora.regex") }
|
||||||
|
|
||||||
|
|
||||||
private val textColorRevert = JButton(Icons.revert)
|
private val textColorRevert = JButton(Icons.revert)
|
||||||
|
|||||||
@@ -15,10 +15,17 @@ import java.awt.BorderLayout
|
|||||||
import java.awt.CardLayout
|
import java.awt.CardLayout
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
|
|
||||||
class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 正在安装的数量
|
||||||
|
*/
|
||||||
|
val installing = AtomicInteger(0)
|
||||||
|
}
|
||||||
|
|
||||||
private val pluginsPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 8))
|
private val pluginsPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 8))
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
@@ -93,6 +100,7 @@ class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
|
if (installing.get() > 0) return
|
||||||
if (isLoading.compareAndSet(false, true)) {
|
if (isLoading.compareAndSet(false, true)) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import org.apache.commons.net.io.Util
|
|||||||
import org.jdesktop.swingx.JXLabel
|
import org.jdesktop.swingx.JXLabel
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -33,11 +32,6 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
private val log = LoggerFactory.getLogger(PluginPanel::class.java)
|
private val log = LoggerFactory.getLogger(PluginPanel::class.java)
|
||||||
private val installed = mutableSetOf<String>()
|
private val installed = mutableSetOf<String>()
|
||||||
private val uninstalled = mutableSetOf<String>()
|
private val uninstalled = mutableSetOf<String>()
|
||||||
|
|
||||||
/**
|
|
||||||
* 正在安装的数量
|
|
||||||
*/
|
|
||||||
private val installing = AtomicInteger(0)
|
|
||||||
private val publicKey = Ed25519.generatePublic(
|
private val publicKey = Ed25519.generatePublic(
|
||||||
Base64.decodeBase64("MCowBQYDK2VwAyEAHPyJ5kt2UHWYUPnWU84DOEhCCUE5FEpzdAbeTCNV31A")
|
Base64.decodeBase64("MCowBQYDK2VwAyEAHPyJ5kt2UHWYUPnWU84DOEhCCUE5FEpzdAbeTCNV31A")
|
||||||
)
|
)
|
||||||
@@ -47,7 +41,7 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
private val updateButton = InstallButton().apply { update = true }
|
private val updateButton = InstallButton().apply { update = true }
|
||||||
private val installButton = InstallButton()
|
private val installButton = InstallButton()
|
||||||
private val uninstallButton = JButton(I18n.getString("termora.settings.plugin.uninstall"))
|
private val uninstallButton = JButton(I18n.getString("termora.settings.plugin.uninstall"))
|
||||||
|
private val installing get() = MarketplacePanel.installing
|
||||||
private val restarter get() = TermoraRestarter.getInstance()
|
private val restarter get() = TermoraRestarter.getInstance()
|
||||||
private val pluginManager get() = PluginManager.getInstance()
|
private val pluginManager get() = PluginManager.getInstance()
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
// Nothing
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
if (key == VisualTerminal.Companion.Written && data is String) {
|
if (key == VisualTerminal.Companion.Written && data is String) {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import app.termora.tree.HostTreeNode
|
|||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -23,6 +26,7 @@ import java.nio.charset.Charset
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
open class SSHHostOptionsPane : OptionsPane() {
|
open class SSHHostOptionsPane : OptionsPane() {
|
||||||
@@ -95,6 +99,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
|
loginScripts = terminalOption.loginScripts,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -138,6 +143,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
terminalOption.environmentTextArea.text = host.options.env
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||||
|
terminalOption.loginScripts.addAll(host.options.loginScripts)
|
||||||
|
|
||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||||
@@ -367,11 +373,11 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun chooseKeyPair() {
|
private fun chooseKeyPair() {
|
||||||
val dialog = KeyManagerDialog(
|
val dialog = KeyManagerDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
owner,
|
||||||
selectMode = true,
|
selectMode = true,
|
||||||
)
|
)
|
||||||
dialog.pack()
|
dialog.pack()
|
||||||
dialog.setLocationRelativeTo(null)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
@@ -486,15 +492,61 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val startupCommandTextField = OutlineTextField()
|
val startupCommandTextField = OutlineTextField()
|
||||||
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
val loginScripts = mutableListOf<LoginScript>()
|
||||||
|
|
||||||
|
|
||||||
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
|
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
|
||||||
|
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
|
||||||
|
private val table = FlatTable()
|
||||||
|
private val model = object : DefaultTableModel() {
|
||||||
|
override fun getRowCount(): Int {
|
||||||
|
return loginScripts.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRow(loginScript: LoginScript) {
|
||||||
|
val rowCount = super.getRowCount()
|
||||||
|
loginScripts.add(loginScript)
|
||||||
|
super.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
|
val loginScript = loginScripts[row]
|
||||||
|
return when (column) {
|
||||||
|
0 -> loginScript.expect
|
||||||
|
1 -> loginScript.send
|
||||||
|
else -> super.getValueAt(row, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val tabbed = FlatTabbedPane()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
addBtn.isFocusable = false
|
||||||
|
editBtn.isFocusable = false
|
||||||
|
deleteBtn.isFocusable = false
|
||||||
|
|
||||||
|
deleteBtn.isEnabled = false
|
||||||
|
editBtn.isEnabled = false
|
||||||
|
|
||||||
|
tabbed.styleMap = mapOf(
|
||||||
|
"focusColor" to DynamicColor("TabbedPane.background"),
|
||||||
|
"hoverColor" to DynamicColor("TabbedPane.background"),
|
||||||
|
)
|
||||||
|
tabbed.tabHeight = UIManager.getInt("TabbedPane.tabHeight") - 4
|
||||||
|
putClientProperty("ContentPanelBorder", BorderFactory.createEmptyBorder())
|
||||||
|
tabbed.addTab(I18n.getString("termora.new-host.general"), getCenterComponent())
|
||||||
|
tabbed.addTab(I18n.getString("termora.new-host.terminal.login-scripts"), getLoginScriptsComponent())
|
||||||
|
add(tabbed, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
environmentTextArea.setFocusTraversalKeys(
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
@@ -521,7 +573,39 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = LoginScriptDialog(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
model.addRow(dialog.loginScript ?: return)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = LoginScriptDialog(owner, loginScripts[table.selectedRow])
|
||||||
|
dialog.isVisible = true
|
||||||
|
loginScripts[table.selectedRow] = dialog.loginScript ?: return
|
||||||
|
model.fireTableRowsUpdated(table.selectedRow, table.selectedRow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
rows.sortDescending()
|
||||||
|
for (row in rows) {
|
||||||
|
loginScripts.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
table.selectionModel.addListSelectionListener {
|
||||||
|
deleteBtn.isEnabled = table.selectedRowCount > 0
|
||||||
|
editBtn.isEnabled = deleteBtn.isEnabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -546,6 +630,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
||||||
@@ -560,6 +645,124 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLoginScriptsComponent(): JComponent {
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
val scrollPane = JScrollPane(table)
|
||||||
|
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.terminal.expect"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.terminal.send"))
|
||||||
|
|
||||||
|
table.putClientProperty(
|
||||||
|
FlatClientProperties.STYLE, mapOf(
|
||||||
|
"showHorizontalLines" to true,
|
||||||
|
"showVerticalLines" to true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
table.model = model
|
||||||
|
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||||
|
table.setDefaultRenderer(
|
||||||
|
Any::class.java,
|
||||||
|
DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER })
|
||||||
|
table.fillsViewportHeight = true
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||||
|
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.Companion.BorderColor)
|
||||||
|
)
|
||||||
|
table.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(addBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(editBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(deleteBtn)
|
||||||
|
|
||||||
|
panel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
panel.add(box, BorderLayout.SOUTH)
|
||||||
|
panel.border = BorderFactory.createEmptyBorder(6, 8, 6, 8)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LoginScriptDialog(
|
||||||
|
owner: Window,
|
||||||
|
var loginScript: LoginScript? = null
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
private val formMargin = "4dlu"
|
||||||
|
private val expectTextField = OutlineTextField()
|
||||||
|
private val sendTextField = OutlineTextField()
|
||||||
|
private val regexToggleBtn = JToggleButton(Icons.regex)
|
||||||
|
.apply { toolTipText = I18n.getString("termora.regex") }
|
||||||
|
private val matchCaseToggleBtn = JToggleButton(Icons.matchCase)
|
||||||
|
.apply { toolTipText = I18n.getString("termora.match-case") }
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.new-host.terminal.login-scripts")
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
init()
|
||||||
|
pack()
|
||||||
|
size = Dimension(max(UIManager.getInt("Dialog.width") - 300, 250), preferredSize.height)
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
toolbar.add(regexToggleBtn)
|
||||||
|
toolbar.add(matchCaseToggleBtn)
|
||||||
|
expectTextField.trailingComponent = toolbar
|
||||||
|
expectTextField.placeholderText = I18n.getString("termora.optional")
|
||||||
|
|
||||||
|
val script = loginScript
|
||||||
|
if (script != null) {
|
||||||
|
expectTextField.text = script.expect
|
||||||
|
sendTextField.text = script.send
|
||||||
|
matchCaseToggleBtn.isSelected = script.matchCase
|
||||||
|
regexToggleBtn.isSelected = script.regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
if (sendTextField.text.isBlank()) {
|
||||||
|
sendTextField.outline = "error"
|
||||||
|
sendTextField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginScript = LoginScript(
|
||||||
|
expect = expectTextField.text,
|
||||||
|
send = sendTextField.text,
|
||||||
|
matchCase = matchCaseToggleBtn.isSelected,
|
||||||
|
regex = regexToggleBtn.isSelected,
|
||||||
|
)
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
loginScript = null
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.expect")}:").xy(1, rows)
|
||||||
|
.add(expectTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.send")}:").xy(1, rows)
|
||||||
|
.add(sendTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
@@ -720,7 +923,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane))
|
val dialog = PortForwardingDialog(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val tunneling = dialog.tunneling ?: return
|
val tunneling = dialog.tunneling ?: return
|
||||||
model.addRow(tunneling)
|
model.addRow(tunneling)
|
||||||
@@ -735,7 +938,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val dialog = PortForwardingDialog(
|
val dialog = PortForwardingDialog(
|
||||||
SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane),
|
owner,
|
||||||
tunnelings[row]
|
tunnelings[row]
|
||||||
)
|
)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
@@ -835,7 +1038,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
init()
|
init()
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,34 +21,40 @@ object WSLSupport {
|
|||||||
|
|
||||||
fun getDistributions(): List<WSLDistribution> {
|
fun getDistributions(): List<WSLDistribution> {
|
||||||
if (isSupported.not()) return emptyList()
|
if (isSupported.not()) return emptyList()
|
||||||
|
|
||||||
val baseKeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"
|
|
||||||
val guids = Advapi32Util.registryGetKeys(WinReg.HKEY_CURRENT_USER, baseKeyPath)
|
|
||||||
val distributions = mutableListOf<WSLDistribution>()
|
val distributions = mutableListOf<WSLDistribution>()
|
||||||
|
|
||||||
for (guid in guids) {
|
try {
|
||||||
val key = baseKeyPath + "\\" + guid
|
val baseKeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"
|
||||||
try {
|
val guids = Advapi32Util.registryGetKeys(WinReg.HKEY_CURRENT_USER, baseKeyPath)
|
||||||
if (Advapi32Util.registryKeyExists(WinReg.HKEY_CURRENT_USER, key)) {
|
|
||||||
val distroName =
|
for (guid in guids) {
|
||||||
Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
|
val key = baseKeyPath + "\\" + guid
|
||||||
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
|
try {
|
||||||
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
|
if (Advapi32Util.registryKeyExists(WinReg.HKEY_CURRENT_USER, key)) {
|
||||||
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
|
val distroName =
|
||||||
distributions.add(
|
Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
|
||||||
WSLDistribution(
|
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
|
||||||
guid = guid,
|
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
|
||||||
flavor = flavor,
|
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
|
||||||
basePath = basePath,
|
distributions.add(
|
||||||
distributionName = distroName
|
WSLDistribution(
|
||||||
|
guid = guid,
|
||||||
|
flavor = flavor,
|
||||||
|
basePath = basePath,
|
||||||
|
distributionName = distroName
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
if (log.isWarnEnabled) {
|
||||||
if (log.isWarnEnabled) {
|
log.warn(e.message, e)
|
||||||
log.warn(e.message, e)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return distributions
|
return distributions
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import org.apache.sshd.sftp.common.SftpConstants
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.nio.file.attribute.BasicFileAttributeView
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.io.path.getLastModifiedTime
|
||||||
import kotlin.io.path.inputStream
|
import kotlin.io.path.inputStream
|
||||||
import kotlin.io.path.outputStream
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
@@ -15,10 +22,13 @@ class FileTransfer(
|
|||||||
private val action: TransferAction,
|
private val action: TransferAction,
|
||||||
priority: Transfer.Priority = Transfer.Priority.Normal,
|
priority: Transfer.Priority = Transfer.Priority.Normal,
|
||||||
) : AbstractTransfer(parentId, source, target, false, priority), Closeable {
|
) : AbstractTransfer(parentId, source, target, false, priority), Closeable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(FileTransfer::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var input: InputStream
|
private lateinit var input: InputStream
|
||||||
private lateinit var output: OutputStream
|
private lateinit var output: OutputStream
|
||||||
|
private val isPreserveModificationTime get() = DatabaseManager.getInstance().sftp.preserveModificationTime
|
||||||
private val closed = AtomicBoolean(false)
|
private val closed = AtomicBoolean(false)
|
||||||
|
|
||||||
override suspend fun transfer(bufferSize: Int): Long {
|
override suspend fun transfer(bufferSize: Int): Long {
|
||||||
@@ -31,7 +41,7 @@ class FileTransfer(
|
|||||||
|
|
||||||
if (::output.isInitialized.not()) {
|
if (::output.isInitialized.not()) {
|
||||||
output = if (action == TransferAction.Overwrite) {
|
output = if (action == TransferAction.Overwrite) {
|
||||||
target().outputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
|
target().outputStream()
|
||||||
} else {
|
} else {
|
||||||
target().outputStream(StandardOpenOption.WRITE, StandardOpenOption.APPEND)
|
target().outputStream(StandardOpenOption.WRITE, StandardOpenOption.APPEND)
|
||||||
}
|
}
|
||||||
@@ -40,6 +50,7 @@ class FileTransfer(
|
|||||||
val buffer = ByteArray(bufferSize)
|
val buffer = ByteArray(bufferSize)
|
||||||
val len = input.read(buffer)
|
val len = input.read(buffer)
|
||||||
if (len <= 0) return 0
|
if (len <= 0) return 0
|
||||||
|
|
||||||
output.write(buffer, 0, len)
|
output.write(buffer, 0, len)
|
||||||
return len.toLong()
|
return len.toLong()
|
||||||
}
|
}
|
||||||
@@ -61,6 +72,26 @@ class FileTransfer(
|
|||||||
if (::output.isInitialized) {
|
if (::output.isInitialized) {
|
||||||
IOUtils.closeQuietly(output)
|
IOUtils.closeQuietly(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPreserveModificationTime) {
|
||||||
|
runCatching {
|
||||||
|
val time = source().getLastModifiedTime()
|
||||||
|
val fs = target().fileSystem
|
||||||
|
// SFTP 比较特殊
|
||||||
|
if (fs is SftpFileSystem && fs.version == SftpConstants.SFTP_V3) {
|
||||||
|
val view = Files.getFileAttributeView(target(), BasicFileAttributeView::class.java)
|
||||||
|
view.setTimes(time, time, null)
|
||||||
|
} else {
|
||||||
|
Files.setLastModifiedTime(target(), time)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
if (it !is UnsupportedOperationException) {
|
||||||
|
log.warn(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,17 +56,24 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
|
|||||||
}
|
}
|
||||||
|
|
||||||
val host = hostManager.getHost(hostId) ?: return
|
val host = hostManager.getHost(hostId) ?: return
|
||||||
|
var selectionPane: TransportSelectionPanel? = null
|
||||||
|
|
||||||
for (i in 0 until tabbed.tabCount) {
|
for (i in 0 until tabbed.tabCount) {
|
||||||
val c = tabbed.getComponentAt(i)
|
val c = tabbed.getComponentAt(i)
|
||||||
if (c is TransportSelectionPanel) {
|
if (c is TransportSelectionPanel) {
|
||||||
if (c.state == TransportSelectionPanel.State.Initialized) {
|
if (c.state == TransportSelectionPanel.State.Initialized) {
|
||||||
c.connect(host)
|
selectionPane = c
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabbed.addSelectionTab()
|
if (selectionPane == null) {
|
||||||
|
selectionPane = tabbed.addSelectionTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionPane.connect(host)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import com.formdev.flatlaf.extras.components.FlatTextField
|
|||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.ui.FlatLineBorder
|
import com.formdev.flatlaf.ui.FlatLineBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -205,7 +206,7 @@ class TransportNavigationPanel(
|
|||||||
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||||
textField.text = StringUtils.EMPTY
|
textField.text = StringUtils.EMPTY
|
||||||
} else {
|
} else {
|
||||||
textField.text = path.absolutePathString()
|
textField.text = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,8 +332,8 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
importMenu.isEnabled = lastNode.isFolder
|
importMenu.isEnabled = lastNode.isFolder
|
||||||
|
|
||||||
// 如果选中了 SSH 服务器,那么才启用
|
// 如果选中了 SSH 服务器,那么才启用
|
||||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == SSHProtocolProvider.PROTOCOL }
|
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { TransferProtocolProvider.valueOf(it.protocol) != null }
|
||||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
openWithSFTPCommand.isEnabled = fullNodes.map { it.host }.any { it.protocol == SSHProtocolProvider.PROTOCOL }
|
||||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ termora.file=File
|
|||||||
termora.explorer=Explorer
|
termora.explorer=Explorer
|
||||||
termora.quit-confirm=Quit {0}?
|
termora.quit-confirm=Quit {0}?
|
||||||
|
|
||||||
|
termora.regex=Regex
|
||||||
|
termora.match-case=Match Case
|
||||||
|
termora.optional=Optional
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=New version
|
termora.update.title=New version
|
||||||
@@ -192,6 +195,9 @@ termora.new-host.terminal.encoding=Encoding
|
|||||||
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
||||||
termora.new-host.terminal.startup-commands=Startup Command
|
termora.new-host.terminal.startup-commands=Startup Command
|
||||||
termora.new-host.terminal.env=Environment
|
termora.new-host.terminal.env=Environment
|
||||||
|
termora.new-host.terminal.login-scripts=Login Scripts
|
||||||
|
termora.new-host.terminal.expect=Expect
|
||||||
|
termora.new-host.terminal.send=Send
|
||||||
|
|
||||||
termora.new-host.serial=Serial
|
termora.new-host.serial=Serial
|
||||||
termora.new-host.serial.port=Port
|
termora.new-host.serial.port=Port
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ termora.file=文件
|
|||||||
termora.explorer=文件管理器
|
termora.explorer=文件管理器
|
||||||
termora.quit-confirm=你要退出 {0} 吗?
|
termora.quit-confirm=你要退出 {0} 吗?
|
||||||
|
|
||||||
|
|
||||||
|
termora.regex=正则表达式
|
||||||
|
termora.match-case=匹配大小写
|
||||||
|
termora.optional=可选的
|
||||||
|
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
@@ -180,6 +186,10 @@ termora.new-host.terminal.encoding=编码
|
|||||||
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
||||||
termora.new-host.terminal.startup-commands=启动命令
|
termora.new-host.terminal.startup-commands=启动命令
|
||||||
termora.new-host.terminal.env=环境
|
termora.new-host.terminal.env=环境
|
||||||
|
termora.new-host.terminal.login-scripts=登录脚本
|
||||||
|
termora.new-host.terminal.expect=预期
|
||||||
|
termora.new-host.terminal.send=发送
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
termora.new-host.serial=串口
|
termora.new-host.serial=串口
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ termora.file=文件
|
|||||||
termora.explorer=檔案管理器
|
termora.explorer=檔案管理器
|
||||||
termora.quit-confirm=你要退出 {0} 嗎?
|
termora.quit-confirm=你要退出 {0} 嗎?
|
||||||
|
|
||||||
|
termora.regex=正規表示式
|
||||||
|
termora.match-case=匹配大小寫
|
||||||
|
termora.optional=可選的
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
@@ -181,6 +185,9 @@ termora.new-host.terminal.encoding=編碼
|
|||||||
termora.new-host.terminal.startup-commands=啟動命令
|
termora.new-host.terminal.startup-commands=啟動命令
|
||||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||||
termora.new-host.terminal.env=環境
|
termora.new-host.terminal.env=環境
|
||||||
|
termora.new-host.terminal.login-scripts=登入腳本
|
||||||
|
termora.new-host.terminal.expect=預期
|
||||||
|
termora.new-host.terminal.send=發送
|
||||||
|
|
||||||
termora.new-host.serial=串口
|
termora.new-host.serial=串口
|
||||||
termora.new-host.serial.port=端口
|
termora.new-host.serial.port=端口
|
||||||
|
|||||||
1
src/main/resources/icons/windows7.svg
Normal file
1
src/main/resources/icons/windows7.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
Reference in New Issue
Block a user