feat: ftp plugin

This commit is contained in:
hstyi
2025-07-08 11:32:13 +08:00
committed by hstyi
parent ecf61bedc4
commit 165d544448
33 changed files with 826 additions and 88 deletions

View File

@@ -1 +1 @@
2.0.0-beta.5 2.0.0-beta.6

View File

@@ -4,7 +4,7 @@ slf4j = "2.0.17"
pty4j = "0.13.6" pty4j = "0.13.6"
tinylog = "2.7.0" tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
flatlaf = "3.6" flatlaf = "3.6.1-SNAPSHOT"
kotlinx-serialization-json = "1.9.0" kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0" commons-codec = "1.18.0"
commons-lang3 = "3.17.0" commons-lang3 = "3.17.0"
@@ -46,7 +46,7 @@ h2 = "2.3.232"
sqlite = "3.50.2.0" sqlite = "3.50.2.0"
jug = "5.1.0" jug = "5.1.0"
semver4j = "6.0.0" semver4j = "6.0.0"
jsvg = "1.4.0" jsvg = "2.0.0"
dom4j = "2.2.0" dom4j = "2.2.0"
[libraries] [libraries]

View File

@@ -2,13 +2,13 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
project.version = "0.0.1" project.version = "0.0.1"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
compileOnly(project(":")) compileOnly(project(":"))
implementation("org.apache.commons:commons-pool2:2.12.1")
testImplementation(project(":"))
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
package app.termora.plugins.ftp 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.Extension
import app.termora.plugin.ExtensionSupport import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin import app.termora.plugin.PaidPlugin
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
} }
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> { override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz) return support.getExtensions(clazz)
} }

View File

@@ -1,22 +1,36 @@
package app.termora.plugins.ftp package app.termora.plugins.ftp
import app.termora.Disposer
import app.termora.Host import app.termora.Host
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils import java.awt.BorderLayout
class FTPProtocolHostPanel : ProtocolHostPanel() { 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 { override fun getHost(): Host {
return Host( return pane.getHost()
name = StringUtils.EMPTY,
protocol = FTPProtocolProvider.PROTOCOL
)
} }
override fun setHost(host: Host) { override fun setHost(host: Host) {
pane.setHost(host)
} }
override fun validateFields(): Boolean { override fun validateFields(): Boolean {
return true return pane.validateFields()
} }
} }

View File

@@ -1,19 +1,20 @@
package app.termora.plugins.ftp package app.termora.plugins.ftp
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension { class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object { companion object {
val instance by lazy { FTPProtocolHostPanelExtension() } val instance = FTPProtocolHostPanelExtension()
} }
override fun getProtocolProvider(): ProtocolProvider { override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.instance return FTPProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return FTPProtocolHostPanel() return FTPProtocolHostPanel()
} }
} }

View File

@@ -1,16 +1,33 @@
package app.termora.plugins.ftp package app.termora.plugins.ftp
import app.termora.AuthenticationType
import app.termora.DynamicIcon import app.termora.DynamicIcon
import app.termora.Icons import app.termora.Icons
import app.termora.protocol.FileObjectHandler import app.termora.ProxyType
import app.termora.protocol.FileObjectRequest import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider 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 { class FTPProtocolProvider private constructor() : TransferProtocolProvider {
companion object { companion object {
val instance by lazy { FTPProtocolProvider() } private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
val instance = FTPProtocolProvider()
const val PROTOCOL = "FTP" const val PROTOCOL = "FTP"
} }
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
return Icons.ftp return Icons.ftp
} }
override fun getFileProvider(): FileProvider { override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
return FTPFileProvider.instance 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 { val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
TODO("Not yet implemented") 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))
}
} }

View File

@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension { class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object { companion object {
val instance by lazy { FTPProtocolProviderExtension() } val instance = FTPProtocolProviderExtension()
} }
override fun getProtocolProvider(): ProtocolProvider { override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.Companion.instance return FTPProtocolProvider.instance
} }
} }

View File

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

View File

@@ -14,7 +14,7 @@
<descriptions> <descriptions>
<description>Connecting to FTP</description> <description>Connecting to FTP</description>
<description language="zh_CN">支持连接到 FTP</description> <description language="zh_CN">支持连接到 FTP</description>
<description language="zh_TW">支援連接到 FTP</description> <description language="zh_TW">支援連接到 FTP</description>
</descriptions> </descriptions>

View File

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

View File

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

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=Passive Mode

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被动模式

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被動模式

View File

@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
project.version = "0.0.2" project.version = "0.0.3"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))

View File

@@ -1,12 +1,22 @@
package app.termora.plugins.smb package app.termora.plugins.smb
import app.termora.transfer.s3.S3FileSystem import app.termora.transfer.s3.S3FileSystem
import app.termora.transfer.s3.S3Path
import com.hierynomus.smbj.session.Session import com.hierynomus.smbj.session.Session
import com.hierynomus.smbj.share.DiskShare import com.hierynomus.smbj.share.DiskShare
class SMBFileSystem(private val share: DiskShare, session: Session) : class SMBFileSystem(private val share: DiskShare, session: Session) :
S3FileSystem(SMBFileSystemProvider(share, 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() { override fun close() {
share.close() share.close()
super.close() super.close()

View File

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

View File

@@ -7,7 +7,7 @@ include("plugins:s3")
include("plugins:oss") include("plugins:oss")
include("plugins:cos") include("plugins:cos")
include("plugins:obs") include("plugins:obs")
//include("plugins:ftp") include("plugins:ftp")
include("plugins:bg") include("plugins:bg")
include("plugins:sync") include("plugins:sync")
include("plugins:migration") include("plugins:migration")

View File

@@ -153,6 +153,7 @@ class TermoraFencePanel(
override fun dispose() { override fun dispose() {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10)) enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
} }

View File

@@ -78,8 +78,6 @@ class BasicProxyOption(
proxyAuthenticationTypeComboBox.addItem(type) proxyAuthenticationTypeComboBox.addItem(type)
} }
proxyUsernameTextField.text = "root"
refreshStates() refreshStates()
} }

View File

@@ -8,6 +8,7 @@ import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.UIScale import com.formdev.flatlaf.util.UIScale
import com.github.weisj.jsvg.SVGDocument import com.github.weisj.jsvg.SVGDocument
import com.github.weisj.jsvg.parser.SVGLoader import com.github.weisj.jsvg.parser.SVGLoader
import com.github.weisj.jsvg.parser.impl.MutableLoaderContext
import java.awt.Component import java.awt.Component
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
@@ -21,8 +22,8 @@ class PluginSVGIcon(input: InputStream, dark: InputStream? = null) : Icon {
} }
private val document = svgLoader.load(input) private val document = svgLoader.load(input, null, MutableLoaderContext.createDefault())
private val darkDocument = dark?.let { svgLoader.load(it) } private val darkDocument = dark?.let { svgLoader.load(it, null, MutableLoaderContext.createDefault()) }
override fun getIconHeight(): Int { override fun getIconHeight(): Int {
return 32 return 32

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import okio.withLock import okio.withLock
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
import org.jdesktop.swingx.treetable.DefaultTreeTableModel import org.jdesktop.swingx.treetable.DefaultTreeTableModel
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -500,6 +501,10 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
} }
override fun dispose() {
removeTransfer(StringUtils.EMPTY)
}
private class UserCanceledException : RuntimeException() private class UserCanceledException : RuntimeException()

View File

@@ -9,6 +9,7 @@ import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.wsl.WSLHostTerminalTab import app.termora.plugin.internal.wsl.WSLHostTerminalTab
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.transfer.TransportTableModel.Attributes import app.termora.transfer.TransportTableModel.Attributes
import app.termora.transfer.s3.S3FileAttributes
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.icons.FlatTreeClosedIcon import com.formdev.flatlaf.icons.FlatTreeClosedIcon
@@ -48,6 +49,7 @@ import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.stream.Stream import java.util.stream.Stream
import javax.swing.* import javax.swing.*
import javax.swing.TransferHandler 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 owner get() = SwingUtilities.getWindowAncestor(this)
private val lru = object : LinkedHashMap<String, Icon?>() { private val lru = object : LinkedHashMap<String, Icon?>() {
override fun removeEldestEntry(eldest: Map.Entry<String?, Icon?>?): Boolean { override fun removeEldestEntry(eldest: Map.Entry<String?, Icon?>?): Boolean {
@@ -113,7 +116,7 @@ internal class TransportPanel(
get() = enableManager.getFlag(showHiddenFilesKey, true) get() = enableManager.getFlag(showHiddenFilesKey, true)
set(value) = enableManager.setFlag(showHiddenFilesKey, value) set(value) = enableManager.setFlag(showHiddenFilesKey, value)
private val navigator get() = this 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 history = linkedSetOf<Path>()
private val undoManager = MyUndoManager() private val undoManager = MyUndoManager()
private val editTransferListener = EditTransferListener() private val editTransferListener = EditTransferListener()
@@ -301,7 +304,7 @@ internal class TransportPanel(
} }
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
if (loading) { if (loading) {
nextReloadCallbacks.add { reload(requestFocus = false) } registerNextReloadCallback { reload(requestFocus = false) }
} else { } else {
reload(requestFocus = false) reload(requestFocus = false)
} }
@@ -620,7 +623,7 @@ internal class TransportPanel(
} }
fun registerSelectRow(name: String) { fun registerSelectRow(name: String) {
nextReloadCallbacks.add { registerNextReloadCallback {
for (i in 0 until model.rowCount) { for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) { if (model.getAttributes(i).name == name) {
val c = sorter.convertRowIndexToView(i) 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( fun reload(
oldPath: String? = workdir?.absolutePathString(), oldPath: String? = workdir?.absolutePathString(),
newPath: String? = workdir?.absolutePathString(), newPath: String? = workdir?.absolutePathString(),
@@ -643,6 +651,8 @@ internal class TransportPanel(
if (loading) return false if (loading) return false
loading = true loading = true
val mod = mod.getAndAdd(1)
coroutineScope.launch { coroutineScope.launch {
try { try {
@@ -650,7 +660,7 @@ internal class TransportPanel(
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
setNewWorkdir(workdir) setNewWorkdir(workdir)
nextReloadCallbacks.forEach { runCatching { it.invoke() } } nextReloadCallbacks[mod]?.forEach { runCatching { it.invoke() } }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -665,7 +675,7 @@ internal class TransportPanel(
} finally { } finally {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
loading = false loading = false
nextReloadCallbacks.clear() nextReloadCallbacks.entries.removeIf { it.key <= mod }
} }
} }
} }
@@ -730,6 +740,13 @@ internal class TransportPanel(
if (files.isNotEmpty()) if (files.isNotEmpty())
consume.invoke() consume.invoke()
if (first.compareAndSet(false, true)) {
withContext(Dispatchers.Swing) {
model.clear()
table.scrollRectToVisible(Rectangle())
}
}
if (requestFocus) if (requestFocus)
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() } coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
@@ -776,7 +793,9 @@ internal class TransportPanel(
.getOrNull() .getOrNull()
val fileSize = basicAttributes?.size() ?: 0 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 owner = fileOwnerAttribute?.name ?: StringUtils.EMPTY
val lastModifiedTime = basicAttributes?.lastModifiedTime()?.toMillis() ?: 0 val lastModifiedTime = basicAttributes?.lastModifiedTime()?.toMillis() ?: 0
val isDirectory = basicAttributes?.isDirectory ?: false val isDirectory = basicAttributes?.isDirectory ?: false

View File

@@ -68,6 +68,7 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
} }
private fun initEvents() { private fun initEvents() {
splitPane.addComponentListener(object : ComponentAdapter() { splitPane.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) { override fun componentResized(e: ComponentEvent) {
removeComponentListener(this) 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, leftTabbed)
Disposer.register(this, rightTabbed) Disposer.register(this, rightTabbed)
} }

View File

@@ -2,6 +2,7 @@ package app.termora.transfer.s3
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime import java.nio.file.attribute.FileTime
import java.nio.file.attribute.PosixFilePermission
data class S3FileAttributes( data class S3FileAttributes(
private val lastModifiedTime: Long = 0, private val lastModifiedTime: Long = 0,
@@ -13,7 +14,12 @@ data class S3FileAttributes(
private val symbolicLink: Boolean = false, private val symbolicLink: Boolean = false,
private val other: Boolean = false, private val other: Boolean = false,
private val size: Long = 0, private val size: Long = 0,
) : BasicFileAttributes {
) : BasicFileAttributes {
var permissions: Set<PosixFilePermission> = emptySet()
override fun lastModifiedTime(): FileTime { override fun lastModifiedTime(): FileTime {
return FileTime.fromMillis(lastModifiedTime) return FileTime.fromMillis(lastModifiedTime)
} }
@@ -49,4 +55,6 @@ data class S3FileAttributes(
override fun fileKey(): Any? { override fun fileKey(): Any? {
return null return null
} }
} }

View File

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

View File

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