feat: ftp plugin
@@ -4,7 +4,7 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
flatlaf = "3.6.1-SNAPSHOT"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
@@ -46,7 +46,7 @@ h2 = "2.3.232"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "1.4.0"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -2,13 +2,13 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||
testImplementation(project(":"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
|
||||
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = FTPPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(pool)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
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.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class FTPHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = FTPProtocolProvider.PROTOCOL
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
port = port,
|
||||
host = generalOption.hostTextField.text,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
|
||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||
.getOrNull() ?: PassiveMode.Local
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (validateField(generalOption.hostTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(c: JComponent) {
|
||||
selectOptionJComponent(c)
|
||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(21)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyComboBox.isEditable = false
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = StringUtils.EMPTY
|
||||
if (value is String) {
|
||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
}
|
||||
|
||||
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.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).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("${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)
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val passiveComboBox = JComboBox<PassiveMode>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
passiveComboBox.addItem(PassiveMode.Local)
|
||||
passiveComboBox.addItem(PassiveMode.Remote)
|
||||
|
||||
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.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
enum class PassiveMode {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object FTPI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(FTPI18n::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.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = FTPHostOptionsPane()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
add(pane, BorderLayout.CENTER)
|
||||
Disposer.register(this, pane)
|
||||
}
|
||||
|
||||
private fun initEvents() {}
|
||||
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
val instance = FTPProtocolHostPanelExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.ProxyType
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||
import org.apache.commons.pool2.PooledObject
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
import java.time.Duration
|
||||
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||
|
||||
val instance = FTPProtocolProvider()
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||
val host = requester.host
|
||||
|
||||
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||
maxTotal = 12
|
||||
// 与 transfer 最大传输量匹配
|
||||
maxIdle = 6
|
||||
minIdle = 1
|
||||
testOnBorrow = false
|
||||
testWhileIdle = true
|
||||
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||
override fun create(): FTPClient {
|
||||
val client = FTPClient()
|
||||
client.charset = Charset.forName(host.options.encoding)
|
||||
client.controlEncoding = client.charset.name()
|
||||
client.connect(host.host, host.port)
|
||||
if (client.isConnected.not()) {
|
||||
throw IllegalStateException("FTP client is not connected")
|
||||
}
|
||||
|
||||
if (host.proxy.type == ProxyType.HTTP) {
|
||||
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
}
|
||||
|
||||
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||
host.authentication.password else StringUtils.EMPTY
|
||||
if (client.login(host.username, password).not()) {
|
||||
throw IllegalStateException("Incorrect account or password")
|
||||
}
|
||||
|
||||
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||
client.enterRemotePassiveMode()
|
||||
} else {
|
||||
client.enterLocalPassiveMode()
|
||||
}
|
||||
|
||||
client.listHiddenFiles = true
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||
return DefaultPooledObject(obj)
|
||||
}
|
||||
|
||||
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||
val ftp = p.`object`
|
||||
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||
}
|
||||
|
||||
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||
try {
|
||||
p.`object`.disconnect()
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, config)
|
||||
|
||||
val defaultPath = host.options.sftpDefaultDirectory
|
||||
val fs = FTPFileSystem(ftpClientPool)
|
||||
return PathHandler(fs, fs.getPath(defaultPath))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
val instance = FTPProtocolProviderExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.net.ftp.FTPFile
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
return createStreamer(path)
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||
return object : InputStream() {
|
||||
override fun read(): Int {
|
||||
return fs.read()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(fs)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStreamer(path: S3Path): OutputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val os = ftp.storeFileStream(path.absolutePathString())
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
if (path.exists().not()) {
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
withFtpClient {
|
||||
val files = it.listFiles(path.absolutePathString())
|
||||
for (file in files) {
|
||||
val p = path.resolve(file.name)
|
||||
p.attributes = p.attributes.copy(
|
||||
directory = file.isDirectory,
|
||||
regularFile = file.isFile,
|
||||
size = file.size,
|
||||
lastModifiedTime = file.timestamp.timeInMillis,
|
||||
)
|
||||
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||
val perms = mutableSetOf<PosixFilePermission>()
|
||||
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_READ)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_READ)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||
}
|
||||
|
||||
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||
if (source != null && target != null) {
|
||||
withFtpClient {
|
||||
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
withFtpClient {
|
||||
if (isDirectory) {
|
||||
it.rmd(path.absolutePathString())
|
||||
} else {
|
||||
it.deleteFile(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
withFtpClient {
|
||||
if (it.cwd(path.absolutePathString()) == 250) {
|
||||
return
|
||||
}
|
||||
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||
val client = pool.borrowObject()
|
||||
return try {
|
||||
block(client)
|
||||
} finally {
|
||||
pool.returnObject(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to FTP</description>
|
||||
<description language="zh_CN">支持连接到到 FTP</description>
|
||||
<description language="zh_CN">支持连接到 FTP</description>
|
||||
<description language="zh_TW">支援連接到 FTP</description>
|
||||
</descriptions>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=Passive Mode
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被动模式
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被動模式
|
||||
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
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 create(root: String?, names: List<String>): S3Path {
|
||||
val path = SMBPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
share.close()
|
||||
super.close()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class SMBPath(fileSystem: SMBFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ include("plugins:s3")
|
||||
include("plugins:oss")
|
||||
include("plugins:cos")
|
||||
include("plugins:obs")
|
||||
//include("plugins:ftp")
|
||||
include("plugins:ftp")
|
||||
include("plugins:bg")
|
||||
include("plugins:sync")
|
||||
include("plugins:migration")
|
||||
|
||||
@@ -153,6 +153,7 @@ class TermoraFencePanel(
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
if (leftTreePanel.isVisible)
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,6 @@ class BasicProxyOption(
|
||||
proxyAuthenticationTypeComboBox.addItem(type)
|
||||
}
|
||||
|
||||
proxyUsernameTextField.text = "root"
|
||||
|
||||
refreshStates()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.util.UIScale
|
||||
import com.github.weisj.jsvg.SVGDocument
|
||||
import com.github.weisj.jsvg.parser.SVGLoader
|
||||
import com.github.weisj.jsvg.parser.impl.MutableLoaderContext
|
||||
import java.awt.Component
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
@@ -21,8 +22,8 @@ class PluginSVGIcon(input: InputStream, dark: InputStream? = null) : Icon {
|
||||
|
||||
}
|
||||
|
||||
private val document = svgLoader.load(input)
|
||||
private val darkDocument = dark?.let { svgLoader.load(it) }
|
||||
private val document = svgLoader.load(input, null, MutableLoaderContext.createDefault())
|
||||
private val darkDocument = dark?.let { svgLoader.load(it, null, MutableLoaderContext.createDefault()) }
|
||||
|
||||
override fun getIconHeight(): Int {
|
||||
return 32
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import okio.withLock
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
||||
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -500,6 +501,10 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
removeTransfer(StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
private class UserCanceledException : RuntimeException()
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transfer.TransportTableModel.Attributes
|
||||
import app.termora.transfer.s3.S3FileAttributes
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
@@ -48,6 +49,7 @@ import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.stream.Stream
|
||||
import javax.swing.*
|
||||
import javax.swing.TransferHandler
|
||||
@@ -83,6 +85,7 @@ internal class TransportPanel(
|
||||
|
||||
}
|
||||
|
||||
private val mod = AtomicLong(0)
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val lru = object : LinkedHashMap<String, Icon?>() {
|
||||
override fun removeEldestEntry(eldest: Map.Entry<String?, Icon?>?): Boolean {
|
||||
@@ -113,7 +116,7 @@ internal class TransportPanel(
|
||||
get() = enableManager.getFlag(showHiddenFilesKey, true)
|
||||
set(value) = enableManager.setFlag(showHiddenFilesKey, value)
|
||||
private val navigator get() = this
|
||||
private val nextReloadCallbacks = mutableListOf<() -> Unit>()
|
||||
private val nextReloadCallbacks = Collections.synchronizedMap(mutableMapOf<Long, MutableList<() -> Unit>>())
|
||||
private val history = linkedSetOf<Path>()
|
||||
private val undoManager = MyUndoManager()
|
||||
private val editTransferListener = EditTransferListener()
|
||||
@@ -301,7 +304,7 @@ internal class TransportPanel(
|
||||
}
|
||||
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||
if (loading) {
|
||||
nextReloadCallbacks.add { reload(requestFocus = false) }
|
||||
registerNextReloadCallback { reload(requestFocus = false) }
|
||||
} else {
|
||||
reload(requestFocus = false)
|
||||
}
|
||||
@@ -620,7 +623,7 @@ internal class TransportPanel(
|
||||
}
|
||||
|
||||
fun registerSelectRow(name: String) {
|
||||
nextReloadCallbacks.add {
|
||||
registerNextReloadCallback {
|
||||
for (i in 0 until model.rowCount) {
|
||||
if (model.getAttributes(i).name == name) {
|
||||
val c = sorter.convertRowIndexToView(i)
|
||||
@@ -633,6 +636,11 @@ internal class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerNextReloadCallback(block: () -> Unit) {
|
||||
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
|
||||
.add(block)
|
||||
}
|
||||
|
||||
fun reload(
|
||||
oldPath: String? = workdir?.absolutePathString(),
|
||||
newPath: String? = workdir?.absolutePathString(),
|
||||
@@ -643,6 +651,8 @@ internal class TransportPanel(
|
||||
if (loading) return false
|
||||
loading = true
|
||||
|
||||
val mod = mod.getAndAdd(1)
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
|
||||
@@ -650,7 +660,7 @@ internal class TransportPanel(
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
setNewWorkdir(workdir)
|
||||
nextReloadCallbacks.forEach { runCatching { it.invoke() } }
|
||||
nextReloadCallbacks[mod]?.forEach { runCatching { it.invoke() } }
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -665,7 +675,7 @@ internal class TransportPanel(
|
||||
} finally {
|
||||
withContext(Dispatchers.Swing) {
|
||||
loading = false
|
||||
nextReloadCallbacks.clear()
|
||||
nextReloadCallbacks.entries.removeIf { it.key <= mod }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,6 +740,13 @@ internal class TransportPanel(
|
||||
if (files.isNotEmpty())
|
||||
consume.invoke()
|
||||
|
||||
if (first.compareAndSet(false, true)) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
model.clear()
|
||||
table.scrollRectToVisible(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
if (requestFocus)
|
||||
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
||||
|
||||
@@ -776,7 +793,9 @@ internal class TransportPanel(
|
||||
.getOrNull()
|
||||
|
||||
val fileSize = basicAttributes?.size() ?: 0
|
||||
val permissions = posixFileAttribute?.permissions() ?: emptySet()
|
||||
val permissions = posixFileAttribute?.permissions()
|
||||
?: if (basicAttributes is S3FileAttributes) basicAttributes.permissions
|
||||
else emptySet()
|
||||
val owner = fileOwnerAttribute?.name ?: StringUtils.EMPTY
|
||||
val lastModifiedTime = basicAttributes?.lastModifiedTime()?.toMillis() ?: 0
|
||||
val isDirectory = basicAttributes?.isDirectory ?: false
|
||||
|
||||
@@ -68,6 +68,7 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
splitPane.addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
removeComponentListener(this)
|
||||
@@ -91,6 +92,8 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
}
|
||||
}
|
||||
|
||||
Disposer.register(this, transferManager)
|
||||
Disposer.register(this, transferTable)
|
||||
Disposer.register(this, leftTabbed)
|
||||
Disposer.register(this, rightTabbed)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.transfer.s3
|
||||
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
|
||||
data class S3FileAttributes(
|
||||
private val lastModifiedTime: Long = 0,
|
||||
@@ -13,7 +14,12 @@ data class S3FileAttributes(
|
||||
private val symbolicLink: Boolean = false,
|
||||
private val other: Boolean = false,
|
||||
private val size: Long = 0,
|
||||
|
||||
) : BasicFileAttributes {
|
||||
|
||||
var permissions: Set<PosixFilePermission> = emptySet()
|
||||
|
||||
|
||||
override fun lastModifiedTime(): FileTime {
|
||||
return FileTime.fromMillis(lastModifiedTime)
|
||||
}
|
||||
@@ -49,4 +55,6 @@ data class S3FileAttributes(
|
||||
override fun fileKey(): Any? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
<svg t="1751945417827" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M876.77610666 73.728H150.63722666A85.54496 85.54496 0 0 0 65.19466666 159.17056v531.57888a85.56544 85.56544 0 0 0 85.44256 85.46304h726.13888c47.104 0 85.44256-38.33856 85.44256-85.46304V159.17056c0-47.104-38.33856-85.44256-85.44256-85.44256zM904.87466666 690.74944c0 15.50336-12.61568 28.09856-28.09856 28.09856H150.63722666A28.13952 28.13952 0 0 1 122.53866666 690.74944V159.17056c0-15.48288 12.61568-28.09856 28.09856-28.09856h726.13888c15.48288 0 28.09856 12.61568 28.09856 28.09856v531.57888zM749.22666666 862.208h-471.04a28.672 28.672 0 0 0 0 57.344h471.04a28.672 28.672 0 0 0 0-57.344z" p-id="1613" fill="#6C707E"></path><path d="M227.04810666 557.568h50.03264v-101.45792h88.92416v-41.90208h-88.92416v-65.26976h104.1408v-41.59488H227.04810666zM404.28202666 348.93824h70.32832v208.62976h50.05312V348.93824h71.00416v-41.59488h-191.3856zM718.46570666 307.34336h-82.16576v250.20416h50.03264v-88.92416h33.4848c53.76 0 96.70656-25.68192 96.70656-82.8416 0-59.16672-42.5984-78.4384-98.05824-78.4384z m-2.02752 121.73312h-30.1056v-82.16576h28.40576c34.48832 0 52.736 9.80992 52.736 38.87104 0.02048 28.75392-16.54784 43.29472-51.03616 43.29472z" p-id="1614" fill="#6C707E"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
<svg t="1751945417827" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M876.77610666 73.728H150.63722666A85.54496 85.54496 0 0 0 65.19466666 159.17056v531.57888a85.56544 85.56544 0 0 0 85.44256 85.46304h726.13888c47.104 0 85.44256-38.33856 85.44256-85.46304V159.17056c0-47.104-38.33856-85.44256-85.44256-85.44256zM904.87466666 690.74944c0 15.50336-12.61568 28.09856-28.09856 28.09856H150.63722666A28.13952 28.13952 0 0 1 122.53866666 690.74944V159.17056c0-15.48288 12.61568-28.09856 28.09856-28.09856h726.13888c15.48288 0 28.09856 12.61568 28.09856 28.09856v531.57888zM749.22666666 862.208h-471.04a28.672 28.672 0 0 0 0 57.344h471.04a28.672 28.672 0 0 0 0-57.344z" p-id="1613" fill="#CED0D6"></path><path d="M227.04810666 557.568h50.03264v-101.45792h88.92416v-41.90208h-88.92416v-65.26976h104.1408v-41.59488H227.04810666zM404.28202666 348.93824h70.32832v208.62976h50.05312V348.93824h71.00416v-41.59488h-191.3856zM718.46570666 307.34336h-82.16576v250.20416h50.03264v-88.92416h33.4848c53.76 0 96.70656-25.68192 96.70656-82.8416 0-59.16672-42.5984-78.4384-98.05824-78.4384z m-2.02752 121.73312h-30.1056v-82.16576h28.40576c34.48832 0 52.736 9.80992 52.736 38.87104 0.02048 28.75392-16.54784 43.29472-51.03616 43.29472z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |