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"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.2"
|
||||
testcontainers = "1.21.3"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
@@ -43,9 +43,9 @@ restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-2"
|
||||
h2 = "2.3.232"
|
||||
sqlite = "3.50.1.0"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "5.8.0"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "1.4.0"
|
||||
dom4j = "2.1.4"
|
||||
|
||||
|
||||
@@ -72,4 +72,8 @@ 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/
|
||||
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 {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.qcloud:cos_api:5.6.245")
|
||||
implementation("com.qcloud:cos_api:5.6.247")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ project.version = "0.0.1"
|
||||
|
||||
dependencies {
|
||||
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(":"))
|
||||
}
|
||||
|
||||
|
||||
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:geo")
|
||||
include("plugins:webdav")
|
||||
include("plugins:smb")
|
||||
|
||||
@@ -2,7 +2,7 @@ package app.termora
|
||||
|
||||
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) {
|
||||
constructor(name: String) : this(name, name)
|
||||
|
||||
|
||||
@@ -88,7 +88,27 @@ data class SerialComm(
|
||||
)
|
||||
|
||||
@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
|
||||
@@ -97,6 +117,10 @@ data class Options(
|
||||
* 跳板机
|
||||
*/
|
||||
val jumpHosts: List<String> = mutableListOf(),
|
||||
/**
|
||||
* 登录脚本
|
||||
*/
|
||||
val loginScripts: List<LoginScript> = emptyList(),
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
|
||||
@@ -55,6 +55,7 @@ class HostManager private constructor() : Disposable {
|
||||
fun getHost(id: String): Host? {
|
||||
val data = databaseManager.data(id) ?: return null
|
||||
if (data.type != DataType.Host.name) return null
|
||||
if (data.deleted) return null
|
||||
return ohMyJson.decodeFromString(data.data)
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ object Icons {
|
||||
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 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 serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
|
||||
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
||||
|
||||
@@ -37,7 +37,7 @@ abstract class PtyHostTerminalTab(
|
||||
}
|
||||
|
||||
// 开启 PTY
|
||||
val ptyConnector = openPtyConnector()
|
||||
val ptyConnector = loginScriptsPtyConnector(host, openPtyConnector())
|
||||
ptyConnectorDelegate.ptyConnector = ptyConnector
|
||||
|
||||
// 开启 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) {
|
||||
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)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
|
||||
@@ -5,14 +5,19 @@ import app.termora.Application.ohMyJson
|
||||
import app.termora.account.Account
|
||||
import app.termora.account.AccountExtension
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.account.AccountOwner
|
||||
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.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.snippet.SnippetManager
|
||||
import app.termora.terminal.CursorStyle
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
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.statements.StatementType
|
||||
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) {
|
||||
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()) {
|
||||
// 已经删除,则忽略
|
||||
if (host.deleted) continue
|
||||
// 不是用户数据,那么忽略
|
||||
if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue
|
||||
// 不是本地用户数据,那么忽略
|
||||
if (AccountManager.isLocally(host.ownerId).not()) continue
|
||||
// 转移资产
|
||||
val newHost = host.copy(
|
||||
id = randomUUID(),
|
||||
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)
|
||||
// 先删除,因为 ID 没有改变,改变的只是 owner 信息
|
||||
silentDelete(host.id)
|
||||
hostManager.addHost(host.copy(ownerId = accountOwner.id, ownerType = accountOwner.type.name))
|
||||
}
|
||||
|
||||
if (deleteIds.isNotEmpty()) {
|
||||
lock.withLock {
|
||||
transaction(database) {
|
||||
DataEntity.deleteWhere { DataEntity.id.inList(deleteIds) }
|
||||
}
|
||||
}
|
||||
for (snippet in snippetManager.snippets()) {
|
||||
if (snippet.deleted) continue
|
||||
silentDelete(snippet.id)
|
||||
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 {
|
||||
|
||||
@@ -45,8 +45,8 @@ class NewKeywordHighlightDialog(
|
||||
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
|
||||
I18n.getString("termora.highlight.background-color")
|
||||
)
|
||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
||||
val regexBtn = JToggleButton(Icons.regex)
|
||||
val matchCaseBtn = JToggleButton(Icons.matchCase).apply { toolTipText = I18n.getString("termora.match-case") }
|
||||
val regexBtn = JToggleButton(Icons.regex).apply { toolTipText = I18n.getString("termora.regex") }
|
||||
|
||||
|
||||
private val textColorRevert = JButton(Icons.revert)
|
||||
|
||||
@@ -15,10 +15,17 @@ import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.event.ActionEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.swing.*
|
||||
|
||||
|
||||
class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
||||
companion object {
|
||||
/**
|
||||
* 正在安装的数量
|
||||
*/
|
||||
val installing = AtomicInteger(0)
|
||||
}
|
||||
|
||||
private val pluginsPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 8))
|
||||
private val cardLayout = CardLayout()
|
||||
@@ -93,6 +100,7 @@ class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
if (installing.get() > 0) return
|
||||
if (isLoading.compareAndSet(false, true)) {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Swing) {
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.apache.commons.net.io.Util
|
||||
import org.jdesktop.swingx.JXLabel
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.swing.*
|
||||
@@ -33,11 +32,6 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
||||
private val log = LoggerFactory.getLogger(PluginPanel::class.java)
|
||||
private val installed = mutableSetOf<String>()
|
||||
private val uninstalled = mutableSetOf<String>()
|
||||
|
||||
/**
|
||||
* 正在安装的数量
|
||||
*/
|
||||
private val installing = AtomicInteger(0)
|
||||
private val publicKey = Ed25519.generatePublic(
|
||||
Base64.decodeBase64("MCowBQYDK2VwAyEAHPyJ5kt2UHWYUPnWU84DOEhCCUE5FEpzdAbeTCNV31A")
|
||||
)
|
||||
@@ -47,7 +41,7 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
||||
private val updateButton = InstallButton().apply { update = true }
|
||||
private val installButton = InstallButton()
|
||||
private val uninstallButton = JButton(I18n.getString("termora.settings.plugin.uninstall"))
|
||||
|
||||
private val installing get() = MarketplacePanel.installing
|
||||
private val restarter get() = TermoraRestarter.getInstance()
|
||||
private val pluginManager get() = PluginManager.getInstance()
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
@@ -188,6 +188,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||
// Nothing
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
if (key == VisualTerminal.Companion.Written && data is String) {
|
||||
|
||||
@@ -9,6 +9,9 @@ import app.termora.tree.HostTreeNode
|
||||
import app.termora.tree.NewHostTreeDialog
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
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.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
@@ -23,6 +26,7 @@ import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
open class SSHHostOptionsPane : OptionsPane() {
|
||||
@@ -95,6 +99,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||
loginScripts = terminalOption.loginScripts,
|
||||
)
|
||||
|
||||
return Host(
|
||||
@@ -138,6 +143,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
terminalOption.environmentTextArea.text = host.options.env
|
||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||
terminalOption.loginScripts.addAll(host.options.loginScripts)
|
||||
|
||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||
@@ -367,11 +373,11 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
|
||||
private fun chooseKeyPair() {
|
||||
val dialog = KeyManagerDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
owner,
|
||||
selectMode = true,
|
||||
)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(null)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.isVisible = true
|
||||
|
||||
val selectedItem = publicKeyComboBox.selectedItem
|
||||
@@ -486,15 +492,61 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
val startupCommandTextField = OutlineTextField()
|
||||
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
||||
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 {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -521,7 +573,39 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
}
|
||||
|
||||
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
|
||||
val step = 2
|
||||
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(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
||||
@@ -560,6 +645,124 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
|
||||
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 {
|
||||
@@ -720,7 +923,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane))
|
||||
val dialog = PortForwardingDialog(owner)
|
||||
dialog.isVisible = true
|
||||
val tunneling = dialog.tunneling ?: return
|
||||
model.addRow(tunneling)
|
||||
@@ -735,7 +938,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
return
|
||||
}
|
||||
val dialog = PortForwardingDialog(
|
||||
SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane),
|
||||
owner,
|
||||
tunnelings[row]
|
||||
)
|
||||
dialog.isVisible = true
|
||||
@@ -835,7 +1038,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||
setLocationRelativeTo(null)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,34 +21,40 @@ object WSLSupport {
|
||||
|
||||
fun getDistributions(): List<WSLDistribution> {
|
||||
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>()
|
||||
|
||||
for (guid in guids) {
|
||||
val key = baseKeyPath + "\\" + guid
|
||||
try {
|
||||
if (Advapi32Util.registryKeyExists(WinReg.HKEY_CURRENT_USER, key)) {
|
||||
val distroName =
|
||||
Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
|
||||
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
|
||||
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
|
||||
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
|
||||
distributions.add(
|
||||
WSLDistribution(
|
||||
guid = guid,
|
||||
flavor = flavor,
|
||||
basePath = basePath,
|
||||
distributionName = distroName
|
||||
try {
|
||||
val baseKeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"
|
||||
val guids = Advapi32Util.registryGetKeys(WinReg.HKEY_CURRENT_USER, baseKeyPath)
|
||||
|
||||
for (guid in guids) {
|
||||
val key = baseKeyPath + "\\" + guid
|
||||
try {
|
||||
if (Advapi32Util.registryKeyExists(WinReg.HKEY_CURRENT_USER, key)) {
|
||||
val distroName =
|
||||
Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
|
||||
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
|
||||
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
|
||||
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
|
||||
distributions.add(
|
||||
WSLDistribution(
|
||||
guid = guid,
|
||||
flavor = flavor,
|
||||
basePath = basePath,
|
||||
distributionName = distroName
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
return distributions
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.database.DatabaseManager
|
||||
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.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.nio.file.attribute.BasicFileAttributeView
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.io.path.getLastModifiedTime
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.outputStream
|
||||
|
||||
@@ -15,10 +22,13 @@ class FileTransfer(
|
||||
private val action: TransferAction,
|
||||
priority: Transfer.Priority = Transfer.Priority.Normal,
|
||||
) : 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 output: OutputStream
|
||||
|
||||
private val isPreserveModificationTime get() = DatabaseManager.getInstance().sftp.preserveModificationTime
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
override suspend fun transfer(bufferSize: Int): Long {
|
||||
@@ -31,7 +41,7 @@ class FileTransfer(
|
||||
|
||||
if (::output.isInitialized.not()) {
|
||||
output = if (action == TransferAction.Overwrite) {
|
||||
target().outputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
|
||||
target().outputStream()
|
||||
} else {
|
||||
target().outputStream(StandardOpenOption.WRITE, StandardOpenOption.APPEND)
|
||||
}
|
||||
@@ -40,6 +50,7 @@ class FileTransfer(
|
||||
val buffer = ByteArray(bufferSize)
|
||||
val len = input.read(buffer)
|
||||
if (len <= 0) return 0
|
||||
|
||||
output.write(buffer, 0, len)
|
||||
return len.toLong()
|
||||
}
|
||||
@@ -61,6 +72,26 @@ class FileTransfer(
|
||||
if (::output.isInitialized) {
|
||||
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
|
||||
var selectionPane: TransportSelectionPanel? = null
|
||||
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val c = tabbed.getComponentAt(i)
|
||||
if (c is TransportSelectionPanel) {
|
||||
if (c.state == TransportSelectionPanel.State.Initialized) {
|
||||
c.connect(host)
|
||||
return
|
||||
selectionPane = c
|
||||
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.ui.FlatLineBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -205,7 +206,7 @@ class TransportNavigationPanel(
|
||||
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||
textField.text = StringUtils.EMPTY
|
||||
} else {
|
||||
textField.text = path.absolutePathString()
|
||||
textField.text = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -332,8 +332,8 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
importMenu.isEnabled = lastNode.isFolder
|
||||
|
||||
// 如果选中了 SSH 服务器,那么才启用
|
||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == SSHProtocolProvider.PROTOCOL }
|
||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { TransferProtocolProvider.valueOf(it.protocol) != null }
|
||||
openWithSFTPCommand.isEnabled = fullNodes.map { it.host }.any { it.protocol == SSHProtocolProvider.PROTOCOL }
|
||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ termora.file=File
|
||||
termora.explorer=Explorer
|
||||
termora.quit-confirm=Quit {0}?
|
||||
|
||||
termora.regex=Regex
|
||||
termora.match-case=Match Case
|
||||
termora.optional=Optional
|
||||
|
||||
# update
|
||||
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.startup-commands=Startup Command
|
||||
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.port=Port
|
||||
|
||||
@@ -13,6 +13,12 @@ termora.file=文件
|
||||
termora.explorer=文件管理器
|
||||
termora.quit-confirm=你要退出 {0} 吗?
|
||||
|
||||
|
||||
termora.regex=正则表达式
|
||||
termora.match-case=匹配大小写
|
||||
termora.optional=可选的
|
||||
|
||||
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
@@ -180,6 +186,10 @@ termora.new-host.terminal.encoding=编码
|
||||
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
||||
termora.new-host.terminal.startup-commands=启动命令
|
||||
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=串口
|
||||
|
||||
@@ -12,6 +12,10 @@ termora.file=文件
|
||||
termora.explorer=檔案管理器
|
||||
termora.quit-confirm=你要退出 {0} 嗎?
|
||||
|
||||
termora.regex=正規表示式
|
||||
termora.match-case=匹配大小寫
|
||||
termora.optional=可選的
|
||||
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
@@ -181,6 +185,9 @@ termora.new-host.terminal.encoding=編碼
|
||||
termora.new-host.terminal.startup-commands=啟動命令
|
||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||
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.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