From 89fa153c1ef5613ba2f985225032d5437657d056 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 5 Jan 2025 20:32:02 +0800 Subject: [PATCH] feat: support SFTP Refs #10 Refs #9 Refs #6 --- README.md | 3 +- build.gradle.kts | 3 + docs/sftp-zh_CN.png | Bin 0 -> 55729 bytes docs/sftp-zh_TW.png | Bin 0 -> 56115 bytes docs/sftp.png | Bin 0 -> 50623 bytes gradle/libs.versions.toml | 3 + src/main/kotlin/app/termora/Application.kt | 32 + src/main/kotlin/app/termora/HostTree.kt | 15 +- src/main/kotlin/app/termora/HostTreeDialog.kt | 119 +++ src/main/kotlin/app/termora/Icons.kt | 7 + .../kotlin/app/termora/SFTPTerminalTab.kt | 53 ++ .../app/termora/SearchableHostTreeModel.kt | 8 +- src/main/kotlin/app/termora/TerminalTab.kt | 5 + .../kotlin/app/termora/TerminalTabDialog.kt | 18 +- src/main/kotlin/app/termora/TerminalTabbed.kt | 30 +- .../app/termora/TerminalTabbedManager.kt | 1 + src/main/kotlin/app/termora/TermoraFrame.kt | 3 - .../QuickCommandFindEverywhereProvider.kt | 23 +- .../app/termora/transport/BookmarkButton.kt | 164 ++++ .../app/termora/transport/BookmarksDialog.kt | 157 ++++ .../app/termora/transport/FileSystemPanel.kt | 787 ++++++++++++++++++ .../app/termora/transport/FileSystemTabbed.kt | 205 +++++ .../termora/transport/FileSystemTableModel.kt | 232 ++++++ .../transport/FileSystemTransportListener.kt | 19 + .../termora/transport/FileTransportPanel.kt | 162 ++++ .../transport/FileTransportTableModel.kt | 123 +++ .../transport/PosixFilePermissionDialog.kt | 148 ++++ .../termora/transport/SftpFileSystemPanel.kt | 316 +++++++ .../kotlin/app/termora/transport/Transport.kt | 274 ++++++ .../termora/transport/TransportListener.kt | 20 + .../app/termora/transport/TransportManager.kt | 129 +++ .../app/termora/transport/TransportPanel.kt | 194 +++++ src/main/resources/i18n/messages.properties | 76 +- .../resources/i18n/messages_zh_CN.properties | 67 ++ .../resources/i18n/messages_zh_TW.properties | 56 +- src/main/resources/icons/bookmarks.svg | 4 + src/main/resources/icons/bookmarksOff.svg | 6 + .../resources/icons/bookmarksOff_dark.svg | 6 + src/main/resources/icons/bookmarks_dark.svg | 4 + src/main/resources/icons/bulletList.svg | 8 + src/main/resources/icons/bulletList_dark.svg | 8 + .../resources/icons/errorIntroduction.svg | 6 + .../icons/errorIntroduction_dark.svg | 6 + src/main/resources/icons/fileTransfer.svg | 4 + .../resources/icons/fileTransfer_dark.svg | 4 + src/main/resources/icons/listFiles.svg | 7 + src/main/resources/icons/listFiles_dark.svg | 7 + src/main/resources/icons/refresh.svg | 7 + src/main/resources/icons/refresh_dark.svg | 7 + src/test/kotlin/app/termora/SFTPTest.kt | 54 ++ 50 files changed, 3567 insertions(+), 23 deletions(-) create mode 100644 docs/sftp-zh_CN.png create mode 100644 docs/sftp-zh_TW.png create mode 100644 docs/sftp.png create mode 100644 src/main/kotlin/app/termora/HostTreeDialog.kt create mode 100644 src/main/kotlin/app/termora/SFTPTerminalTab.kt create mode 100644 src/main/kotlin/app/termora/transport/BookmarkButton.kt create mode 100644 src/main/kotlin/app/termora/transport/BookmarksDialog.kt create mode 100644 src/main/kotlin/app/termora/transport/FileSystemPanel.kt create mode 100644 src/main/kotlin/app/termora/transport/FileSystemTabbed.kt create mode 100644 src/main/kotlin/app/termora/transport/FileSystemTableModel.kt create mode 100644 src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt create mode 100644 src/main/kotlin/app/termora/transport/FileTransportPanel.kt create mode 100644 src/main/kotlin/app/termora/transport/FileTransportTableModel.kt create mode 100644 src/main/kotlin/app/termora/transport/PosixFilePermissionDialog.kt create mode 100644 src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt create mode 100644 src/main/kotlin/app/termora/transport/Transport.kt create mode 100644 src/main/kotlin/app/termora/transport/TransportListener.kt create mode 100644 src/main/kotlin/app/termora/transport/TransportManager.kt create mode 100644 src/main/kotlin/app/termora/transport/TransportPanel.kt create mode 100644 src/main/resources/icons/bookmarks.svg create mode 100644 src/main/resources/icons/bookmarksOff.svg create mode 100644 src/main/resources/icons/bookmarksOff_dark.svg create mode 100644 src/main/resources/icons/bookmarks_dark.svg create mode 100644 src/main/resources/icons/bulletList.svg create mode 100644 src/main/resources/icons/bulletList_dark.svg create mode 100644 src/main/resources/icons/errorIntroduction.svg create mode 100644 src/main/resources/icons/errorIntroduction_dark.svg create mode 100644 src/main/resources/icons/fileTransfer.svg create mode 100644 src/main/resources/icons/fileTransfer_dark.svg create mode 100644 src/main/resources/icons/listFiles.svg create mode 100644 src/main/resources/icons/listFiles_dark.svg create mode 100644 src/main/resources/icons/refresh.svg create mode 100644 src/main/resources/icons/refresh_dark.svg create mode 100644 src/test/kotlin/app/termora/SFTPTest.kt diff --git a/README.md b/README.md index 79d000b..80ebd67 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ ## 功能特性 - 支持 SSH 和本地终端 +- 支持 [SFTP](./docs/sftp-zh_CN.png) 文件传输 - 支持 Windows、macOS、Linux 平台 - 支持 Zmodem 协议 - 支持 SSH 端口转发 @@ -33,7 +34,7 @@ ## 开发 -建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run`即可运行程序。 +建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。 通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。 diff --git a/build.gradle.kts b/build.gradle.kts index 56e76f0..e4e3341 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { testImplementation(libs.jsch) testImplementation(libs.rhino) testImplementation(libs.delight.rhino.sandbox) + testImplementation(platform(libs.testcontainers.bom)) + testImplementation(libs.testcontainers) implementation(libs.slf4j.api) implementation(libs.pty4j) @@ -137,6 +139,7 @@ tasks.register("jpackage") { val buildDir = layout.buildDirectory.get() val options = mutableListOf( "--add-exports java.base/sun.nio.ch=ALL-UNNAMED", + "-XX:+UseZGC", "-XX:+ZGenerational", "-XX:ZUncommit", "-XX:ZUncommitDelay=60", "-Xmx2g", "-XX:+HeapDumpOnOutOfMemoryError", "-Dlogger.console.level=off", diff --git a/docs/sftp-zh_CN.png b/docs/sftp-zh_CN.png new file mode 100644 index 0000000000000000000000000000000000000000..2a1ded3e747d5118746040b0ff58340f1006c101 GIT binary patch literal 55729 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 ThQQazAdsw-f@HxRgQx!wGmO|= literal 0 HcmV?d00001 diff --git a/docs/sftp-zh_TW.png b/docs/sftp-zh_TW.png new file mode 100644 index 0000000000000000000000000000000000000000..3f7df62dc41f59078875255a8b12991e1a0e91f1 GIT binary patch literal 56115 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, Any>()) private lateinit var baseDataDir: File @@ -99,6 +102,10 @@ object Application { return version } + fun isUnknownVersion(): Boolean { + return getVersion().contains("unknown") + } + fun getAppPath(): String { return StringUtils.defaultString(System.getProperty("jpackage.app-path")) } @@ -144,3 +151,28 @@ object Application { } } } + +fun formatBytes(bytes: Long): String { + if (bytes < 1024) return "$bytes B" + + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB") + val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt() + val value = bytes / 1024.0.pow(exp.toDouble()) + + return String.format("%.2f %s", value, units[exp]) +} + +fun formatSeconds(seconds: Long): String { + val days = seconds / 86400 + val hours = (seconds % 86400) / 3600 + val minutes = (seconds % 3600) / 60 + val remainingSeconds = seconds % 60 + + return when { + days > 0 -> "${days}天${hours}小时${minutes}分${remainingSeconds}秒" + hours > 0 -> "${hours}小时${minutes}分${remainingSeconds}秒" + minutes > 0 -> "${minutes}分${remainingSeconds}秒" + else -> "${remainingSeconds}秒" + } +} + diff --git a/src/main/kotlin/app/termora/HostTree.kt b/src/main/kotlin/app/termora/HostTree.kt index 519a41d..6b26e48 100644 --- a/src/main/kotlin/app/termora/HostTree.kt +++ b/src/main/kotlin/app/termora/HostTree.kt @@ -27,6 +27,13 @@ class HostTree : JTree(), Disposable { private val hostManager get() = HostManager.instance private val editor = OutlineTextField(64) + var contextmenu = true + + /** + * 双击是否打开连接 + */ + var doubleClickConnection = true + val model = HostTreeModel() val searchableModel = SearchableHostTreeModel(model) @@ -122,7 +129,7 @@ class HostTree : JTree(), Disposable { } override fun mouseClicked(e: MouseEvent) { - if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { + if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { val host = lastSelectedPathComponent if (host is Host && host.protocol != Protocol.Folder) { ActionManager.getInstance().getAction(Actions.OPEN_HOST) @@ -296,6 +303,8 @@ class HostTree : JTree(), Disposable { } private fun showContextMenu(event: MouseEvent) { + if (!contextmenu) return + val lastHost = lastSelectedPathComponent if (lastHost !is Host) { return @@ -356,7 +365,7 @@ class HostTree : JTree(), Disposable { remove.addActionListener { if (OptionPane.showConfirmDialog( SwingUtilities.getWindowAncestor(this), - "删除后无法恢复,你确定要删除吗?", + I18n.getString("termora.keymgr.delete-warning"), I18n.getString("termora.remove"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE @@ -512,7 +521,7 @@ class HostTree : JTree(), Disposable { collapsePath(TreePath(model.getPathToRoot(node))) } - private fun getSelectionNodes(): List { + fun getSelectionNodes(): List { val selectionNodes = selectionModel.selectionPaths.map { it.lastPathComponent } .filterIsInstance() diff --git a/src/main/kotlin/app/termora/HostTreeDialog.kt b/src/main/kotlin/app/termora/HostTreeDialog.kt new file mode 100644 index 0000000..8dabdfe --- /dev/null +++ b/src/main/kotlin/app/termora/HostTreeDialog.kt @@ -0,0 +1,119 @@ +package app.termora + +import app.termora.db.Database +import java.awt.Dimension +import java.awt.Window +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.* +import javax.swing.tree.TreeSelectionModel + +class HostTreeDialog(owner: Window) : DialogWrapper(owner) { + + private val tree = HostTree() + + val hosts = mutableListOf() + + var allowMulti = true + set(value) { + field = value + if (value) { + tree.selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION + } else { + tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION + } + } + + init { + size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150) + isModal = true + isResizable = false + controlsVisible = false + title = I18n.getString("termora.transport.sftp.select-host") + + tree.setModel(SearchableHostTreeModel(tree.model) { host -> + host.protocol == Protocol.Folder || host.protocol == Protocol.SSH + }) + tree.contextmenu = true + tree.doubleClickConnection = false + tree.dragEnabled = false + + initEvents() + + init() + setLocationRelativeTo(null) + + } + + private fun initEvents() { + addWindowListener(object : WindowAdapter() { + override fun windowActivated(e: WindowEvent) { + removeWindowListener(this) + val state = Database.instance.properties.getString("HostTreeDialog.HostTreeExpansionState") + if (state != null) { + TreeUtils.loadExpansionState(tree, state) + } + } + }) + + tree.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { + val node = tree.lastSelectedPathComponent ?: return + if (node is Host && node.protocol != Protocol.Folder) { + doOKAction() + } + } + } + }) + + addWindowListener(object : WindowAdapter() { + override fun windowClosed(e: WindowEvent) { + Database.instance.properties.putString( + "HostTreeDialog.HostTreeExpansionState", + TreeUtils.saveExpansionState(tree) + ) + } + }) + } + + override fun createCenterPanel(): JComponent { + val scrollPane = JScrollPane(tree) + scrollPane.border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor), + BorderFactory.createEmptyBorder(4, 6, 4, 6) + ) + + return scrollPane + } + + override fun doOKAction() { + + if (allowMulti) { + val nodes = tree.getSelectionNodes().filter { it.protocol == Protocol.SSH } + if (nodes.isEmpty()) { + return + } + hosts.clear() + hosts.addAll(nodes) + } else { + val node = tree.lastSelectedPathComponent ?: return + if (node !is Host || node.protocol != Protocol.SSH) { + return + } + hosts.clear() + hosts.add(node) + } + + + super.doOKAction() + } + + override fun doCancelAction() { + hosts.clear() + super.doCancelAction() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 042653c..78bb502 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -1,6 +1,7 @@ package app.termora object Icons { + val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") } val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") } val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") } val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") } @@ -14,6 +15,7 @@ object Icons { val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") } val empty by lazy { DynamicIcon("icons/empty.svg") } val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") } + val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") } val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") } val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") } val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") } @@ -26,6 +28,8 @@ object Icons { val rec by lazy { DynamicIcon("icons/rec.svg", "icons/rec_dark.svg") } val stop by lazy { DynamicIcon("icons/stop.svg", "icons/stop_dark.svg") } val find by lazy { DynamicIcon("icons/find.svg", "icons/find_dark.svg") } + val bookmarks by lazy { DynamicIcon("icons/bookmarks.svg", "icons/bookmarks_dark.svg") } + val bookmarksOff by lazy { DynamicIcon("icons/bookmarksOff.svg", "icons/bookmarksOff_dark.svg") } val keyboard by lazy { DynamicIcon("icons/keyboard.svg", "icons/keyboard_dark.svg") } val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") } val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") } @@ -64,9 +68,12 @@ object Icons { val split by lazy { DynamicIcon("icons/split.svg", "icons/split_dark.svg") } val setKey by lazy { DynamicIcon("icons/setKey.svg", "icons/setKey_dark.svg") } val greyKey by lazy { DynamicIcon("icons/greyKey.svg", "icons/greyKey_dark.svg") } + val refresh by lazy { DynamicIcon("icons/refresh.svg", "icons/refresh_dark.svg") } val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") } val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") } val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") } + val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") } + val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") } val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") } val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") } val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") } diff --git a/src/main/kotlin/app/termora/SFTPTerminalTab.kt b/src/main/kotlin/app/termora/SFTPTerminalTab.kt new file mode 100644 index 0000000..5de5e93 --- /dev/null +++ b/src/main/kotlin/app/termora/SFTPTerminalTab.kt @@ -0,0 +1,53 @@ +package app.termora + +import app.termora.transport.TransportPanel +import java.beans.PropertyChangeListener +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JOptionPane +import javax.swing.SwingUtilities + +class SFTPTerminalTab : Disposable, TerminalTab { + + private val transportPanel by lazy { + TransportPanel().apply { + Disposer.register(this@SFTPTerminalTab, this) + } + } + + override fun getTitle(): String { + return "SFTP" + } + + override fun getIcon(): Icon { + return Icons.fileTransfer + } + + override fun addPropertyChangeListener(listener: PropertyChangeListener) { + + } + + override fun removePropertyChangeListener(listener: PropertyChangeListener) { + } + + override fun getJComponent(): JComponent { + return transportPanel + } + + + override fun canClose(): Boolean { + assertEventDispatchThread() + + if (transportPanel.transportManager.getTransports().isEmpty()) { + return true + } + + return OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(getJComponent()), + I18n.getString("termora.transport.sftp.close-tab"), + messageType = JOptionPane.QUESTION_MESSAGE, + optionType = JOptionPane.OK_CANCEL_OPTION + ) == JOptionPane.OK_OPTION + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/SearchableHostTreeModel.kt b/src/main/kotlin/app/termora/SearchableHostTreeModel.kt index e8974f5..08bee70 100644 --- a/src/main/kotlin/app/termora/SearchableHostTreeModel.kt +++ b/src/main/kotlin/app/termora/SearchableHostTreeModel.kt @@ -5,7 +5,10 @@ import javax.swing.event.TreeModelListener import javax.swing.tree.TreeModel import javax.swing.tree.TreePath -class SearchableHostTreeModel(private val model: HostTreeModel) : TreeModel { +class SearchableHostTreeModel( + private val model: HostTreeModel, + private val filter: (host: Host) -> Boolean = { true } +) : TreeModel { private var text = String() override fun getRoot(): Any { @@ -45,7 +48,8 @@ class SearchableHostTreeModel(private val model: HostTreeModel) : TreeModel { val children = model.getChildren(parent) if (children.isEmpty()) return emptyList() return children.filter { e -> - e.name.contains(text, true) || TreeUtils.children(model, e, true).filterIsInstance().any { + filter.invoke(e) && e.name.contains(text, true) || TreeUtils.children(model, e, true) + .filterIsInstance().any { it.name.contains(text, true) } } diff --git a/src/main/kotlin/app/termora/TerminalTab.kt b/src/main/kotlin/app/termora/TerminalTab.kt index 617935c..514ce0a 100644 --- a/src/main/kotlin/app/termora/TerminalTab.kt +++ b/src/main/kotlin/app/termora/TerminalTab.kt @@ -37,5 +37,10 @@ interface TerminalTab : Disposable { fun onLostFocus() {} fun onGrabFocus() {} + /** + * @return 返回 false 则不可关闭 + */ + fun canClose(): Boolean = true + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabDialog.kt b/src/main/kotlin/app/termora/TerminalTabDialog.kt index 8d2ad0d..a82077c 100644 --- a/src/main/kotlin/app/termora/TerminalTabDialog.kt +++ b/src/main/kotlin/app/termora/TerminalTabDialog.kt @@ -3,9 +3,9 @@ package app.termora import java.awt.BorderLayout import java.awt.Dimension import java.awt.Window -import javax.swing.BorderFactory -import javax.swing.JComponent -import javax.swing.JPanel +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.* class TerminalTabDialog( owner: Window, @@ -19,10 +19,20 @@ class TerminalTabDialog( isAlwaysOnTop = false iconImages = owner.iconImages escapeDispose = false - + super.setSize(size) init() + + defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + if (terminalTab.canClose()) { + SwingUtilities.invokeLater { doCancelAction() } + } + } + }) + setLocationRelativeTo(null) } diff --git a/src/main/kotlin/app/termora/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index 219b9f7..1d4ad9b 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -162,7 +162,7 @@ class TerminalTabbed( override fun mouseClicked(e: MouseEvent) { if (SwingUtilities.isLeftMouseButton(e)) { val index = tabbedPane.indexAtLocation(e.x, e.y) - if (index >= 0) { + if (index > 0) { tabbedPane.getComponentAt(index).requestFocusInWindow() } } @@ -213,6 +213,13 @@ class TerminalTabbed( private fun removeTabAt(index: Int, disposable: Boolean = true) { if (tabbedPane.isTabClosable(index)) { val tab = tabs[index] + + if (disposable) { + if (!tab.canClose()) { + return + } + } + tab.onLostFocus() tab.removePropertyChangeListener(iconListener) @@ -244,6 +251,7 @@ class TerminalTabbed( val popupMenu = FlatPopupMenu() + // 修改名称 val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename")) rename.addActionListener { val index = tabbedPane.selectedIndex @@ -261,6 +269,7 @@ class TerminalTabbed( } + // 克隆 val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone")) clone.addActionListener { val index = tabbedPane.selectedIndex @@ -272,9 +281,9 @@ class TerminalTabbed( .actionPerformed(OpenHostActionEvent(this, tab.host)) } } - } + // 在新窗口中打开 val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window")) openInNewWindow.addActionListener { val index = tabbedPane.selectedIndex @@ -294,11 +303,13 @@ class TerminalTabbed( popupMenu.addSeparator() + // 关闭 val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close")) close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) } + // 关闭其他标签页 popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener { for (i in tabbedPane.tabCount - 1 downTo tabIndex + 1) { tabbedPane.tabCloseCallback?.accept(tabbedPane, i) @@ -308,6 +319,7 @@ class TerminalTabbed( } } + // 关闭所有标签页 popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-all-tabs")).addActionListener { for (i in 0 until tabbedPane.tabCount) { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.tabCount - 1) @@ -320,6 +332,11 @@ class TerminalTabbed( clone.isEnabled = close.isEnabled openInNewWindow.isEnabled = close.isEnabled + // SFTP不允许克隆 + if (clone.isEnabled && getSelectedTerminalTab() is SFTPTerminalTab) { + clone.isEnabled = false + } + if (close.isEnabled) { popupMenu.addSeparator() @@ -400,5 +417,14 @@ class TerminalTabbed( return tabs } + override fun setSelectedTerminalTab(tab: TerminalTab) { + for (index in tabs.indices) { + if (tabs[index] == tab) { + tabbedPane.selectedIndex = index + break + } + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabbedManager.kt b/src/main/kotlin/app/termora/TerminalTabbedManager.kt index a9521c6..9ba4c2d 100644 --- a/src/main/kotlin/app/termora/TerminalTabbedManager.kt +++ b/src/main/kotlin/app/termora/TerminalTabbedManager.kt @@ -4,4 +4,5 @@ interface TerminalTabbedManager { fun addTerminalTab(tab: TerminalTab) fun getSelectedTerminalTab(): TerminalTab? fun getTerminalTabs(): List + fun setSelectedTerminalTab(tab: TerminalTab) } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index 023e7b9..58c1fdd 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -392,9 +392,6 @@ class TermoraFrame : JFrame() { if (e.source == tabbedPane) { val index = tabbedPane.indexAtLocation(e.x, e.y) if (index >= 0) { - if (e.id == MouseEvent.MOUSE_CLICKED) { - tabbedPane.getComponentAt(index)?.requestFocusInWindow() - } return } } diff --git a/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt b/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt index 3203e7d..ebe456b 100644 --- a/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt +++ b/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt @@ -1,10 +1,9 @@ package app.termora.findeverywhere -import app.termora.Actions -import app.termora.I18n -import app.termora.Icons +import app.termora.* import com.formdev.flatlaf.FlatLaf import org.jdesktop.swingx.action.ActionManager +import java.awt.event.ActionEvent import javax.swing.Icon class QuickCommandFindEverywhereProvider : FindEverywhereProvider { @@ -15,6 +14,24 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider { ActionManager.getInstance().getAction(Actions.ADD_HOST)?.let { list.add(CreateHostFindEverywhereResult()) } + + // SFTP + list.add(ActionFindEverywhereResult(object : AnAction("SFTP", Icons.fileTransfer) { + override fun actionPerformed(evt: ActionEvent) { + val terminalTabbedManager = Application.getService(TerminalTabbedManager::class) + val tabs = terminalTabbedManager.getTerminalTabs() + for (i in tabs.indices) { + val tab = tabs[i] + if (tab is SFTPTerminalTab) { + terminalTabbedManager.setSelectedTerminalTab(tab) + return + } + } + // 创建一个新的 + terminalTabbedManager.addTerminalTab(SFTPTerminalTab()) + } + })) + return list } diff --git a/src/main/kotlin/app/termora/transport/BookmarkButton.kt b/src/main/kotlin/app/termora/transport/BookmarkButton.kt new file mode 100644 index 0000000..756807e --- /dev/null +++ b/src/main/kotlin/app/termora/transport/BookmarkButton.kt @@ -0,0 +1,164 @@ +package app.termora.transport + +import app.termora.Application.ohMyJson +import app.termora.DynamicColor +import app.termora.I18n +import app.termora.Icons +import app.termora.assertEventDispatchThread +import app.termora.db.Database +import com.formdev.flatlaf.FlatLaf +import com.formdev.flatlaf.extras.components.FlatPopupMenu +import com.formdev.flatlaf.ui.FlatUIUtils +import kotlinx.serialization.encodeToString +import org.apache.commons.lang3.StringUtils +import java.awt.* +import java.awt.event.ActionEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JButton +import javax.swing.SwingConstants +import javax.swing.SwingUtilities + +class BookmarkButton : JButton(Icons.bookmarks) { + private val properties by lazy { Database.instance.properties } + private val arrowWidth = 16 + private val arrowSize = 6 + + /** + * 为 true 表示在书签内 + */ + var isBookmark = false + set(value) { + field = value + icon = if (value) { + Icons.bookmarksOff + } else { + Icons.bookmarks + } + } + + + init { + val oldWidth = preferredSize.width + + preferredSize = Dimension(oldWidth + arrowWidth, preferredSize.height) + horizontalAlignment = SwingConstants.LEFT + + + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (SwingUtilities.isLeftMouseButton(e)) { + if (e.x < oldWidth) { + super@BookmarkButton.fireActionPerformed( + ActionEvent( + this@BookmarkButton, + ActionEvent.ACTION_PERFORMED, + StringUtils.EMPTY + ) + ) + } else { + showBookmarks(e) + } + } + } + }) + + isBookmark = false + } + + private fun showBookmarks(e: MouseEvent) { + if (StringUtils.isBlank(name)) return + + val popupMenu = FlatPopupMenu() + val bookmarks = getBookmarks() + popupMenu.add(I18n.getString("termora.transport.bookmarks")).addActionListener { + val list = BookmarksDialog(SwingUtilities.getWindowAncestor(this), bookmarks).open() + properties.putString(name, ohMyJson.encodeToString(list)) + } + + if (bookmarks.isNotEmpty()) { + popupMenu.addSeparator() + for (bookmark in bookmarks) { + popupMenu.add(bookmark).addActionListener { + super@BookmarkButton.fireActionPerformed( + ActionEvent( + this@BookmarkButton, + ActionEvent.ACTION_PERFORMED, + bookmark + ) + ) + } + } + } + + + + popupMenu.show(e.component, -(popupMenu.preferredSize.width / 2 - width / 2), height + 2) + } + + fun addBookmark(text: String) { + assertEventDispatchThread() + if (StringUtils.isBlank(name)) return + val bookmarks = getBookmarks().toMutableList() + bookmarks.add(text) + properties.putString(name, ohMyJson.encodeToString(bookmarks)) + } + + fun deleteBookmark(text: String) { + assertEventDispatchThread() + if (StringUtils.isBlank(name)) return + val bookmarks = getBookmarks().toMutableList() + bookmarks.removeIf { text == it } + properties.putString(name, ohMyJson.encodeToString(bookmarks)) + } + + fun getBookmarks(): List { + if (StringUtils.isBlank(name)) { + return emptyList() + } + + + val text = properties.getString(name, "[]") + if (StringUtils.isNotBlank(text)) { + runCatching { ohMyJson.decodeFromString>(text) }.onSuccess { + return it + } + } + + + return emptyList() + } + + override fun paintComponent(g: Graphics) { + val g2d = g as Graphics2D + super.paintComponent(g2d) + + val x = preferredSize.width - arrowWidth + + g.color = DynamicColor.BorderColor + g.drawLine(x + 1, 4, x + 1, preferredSize.height - 2) + + g.color = if (FlatLaf.isLafDark()) Color(206, 208, 214) else Color(108, 112, 126) + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + FlatUIUtils.paintArrow( + g2d, x, preferredSize.height / 2 - arrowSize, arrowWidth, arrowWidth, SwingConstants.SOUTH, + false, arrowSize, 0f, 0f, 0f + ) + + + } + + override fun isSelected(): Boolean { + return false + } + + /** + * 忽略默认的触发事件 + */ + override fun fireActionPerformed(event: ActionEvent) { + + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/BookmarksDialog.kt b/src/main/kotlin/app/termora/transport/BookmarksDialog.kt new file mode 100644 index 0000000..fc48629 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/BookmarksDialog.kt @@ -0,0 +1,157 @@ +package app.termora.transport + +import app.termora.DialogWrapper +import app.termora.DynamicColor +import app.termora.I18n +import app.termora.OptionPane +import com.formdev.flatlaf.util.SystemInfo +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Window +import javax.swing.* +import javax.swing.border.EmptyBorder + +class BookmarksDialog( + owner: Window, + bookmarks: List +) : DialogWrapper(owner) { + + private val model = DefaultListModel() + private val list = JList(model) + + private val upBtn = JButton(I18n.getString("termora.transport.bookmarks.up")) + private val downBtn = JButton(I18n.getString("termora.transport.bookmarks.down")) + private val deleteBtn = JButton(I18n.getString("termora.remove")) + + + init { + size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")) + isModal = true + title = I18n.getString("termora.transport.bookmarks") + + initView() + initEvents() + + model.addAll(bookmarks) + + + init() + setLocationRelativeTo(null) + } + + private fun initView() { + + upBtn.isEnabled = false + downBtn.isEnabled = false + deleteBtn.isEnabled = false + + upBtn.isFocusable = false + downBtn.isFocusable = false + deleteBtn.isFocusable = false + + list.fixedCellHeight = UIManager.getInt("Tree.rowHeight") + list.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + + } + + private fun initEvents() { + + upBtn.addActionListener { + val rows = list.selectedIndices.sorted() + list.clearSelection() + + for (row in rows) { + val a = model.getElementAt(row - 1) + val b = model.getElementAt(row) + model.setElementAt(b, row - 1) + model.setElementAt(a, row) + list.selectionModel.addSelectionInterval(row - 1, row - 1) + } + } + + downBtn.addActionListener { + val rows = list.selectedIndices.sortedDescending() + list.clearSelection() + + for (row in rows) { + val a = model.getElementAt(row + 1) + val b = model.getElementAt(row) + model.setElementAt(b, row + 1) + model.setElementAt(a, row) + list.selectionModel.addSelectionInterval(row + 1, row + 1) + } + } + + deleteBtn.addActionListener { + if (list.selectionModel.selectedItemsCount > 0) { + if (OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(this), + I18n.getString("termora.keymgr.delete-warning"), + messageType = JOptionPane.WARNING_MESSAGE + ) == JOptionPane.YES_OPTION + ) { + for (e in list.selectionModel.selectedIndices.sortedDescending()) { + model.removeElementAt(e) + } + + if (model.size > 0) { + list.selectedIndex = 0 + } + } + } + } + + + list.selectionModel.addListSelectionListener { + upBtn.isEnabled = list.selectionModel.selectedItemsCount > 0 + downBtn.isEnabled = upBtn.isEnabled + deleteBtn.isEnabled = upBtn.isEnabled + + upBtn.isEnabled = list.minSelectionIndex != 0 + downBtn.isEnabled = list.maxSelectionIndex != model.size - 1 + } + } + + override fun createCenterPanel(): JComponent { + + val panel = JPanel(BorderLayout()) + panel.add(JScrollPane(list).apply { + border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor) + }, BorderLayout.CENTER) + + var rows = 1 + val step = 2 + val formMargin = "4dlu" + val layout = FormLayout( + "default:grow", + "pref, $formMargin, pref, $formMargin, pref" + ) + panel.add( + FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0)) + .add(upBtn).xy(1, rows).apply { rows += step } + .add(downBtn).xy(1, rows).apply { rows += step } + .add(deleteBtn).xy(1, rows).apply { rows += step } + .build(), + BorderLayout.EAST) + + panel.border = BorderFactory.createEmptyBorder( + if (SystemInfo.isWindows || SystemInfo.isLinux) 6 else 0, + 12, 12, 12 + ) + + return panel + } + + override fun createSouthPanel(): JComponent? { + return null + } + + fun open(): List { + isModal = true + isVisible = true + return model.elements().toList() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileSystemPanel.kt b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt new file mode 100644 index 0000000..cb5b1d3 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt @@ -0,0 +1,787 @@ +package app.termora.transport + +import app.termora.* +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.extras.components.FlatPopupMenu +import com.formdev.flatlaf.extras.components.FlatToolBar +import com.formdev.flatlaf.icons.FlatFileViewDirectoryIcon +import com.formdev.flatlaf.icons.FlatFileViewFileIcon +import com.formdev.flatlaf.util.SystemInfo +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.SystemUtils +import org.apache.commons.lang3.exception.ExceptionUtils +import org.apache.sshd.sftp.client.SftpClient +import org.apache.sshd.sftp.client.fs.SftpFileSystem +import org.apache.sshd.sftp.client.fs.SftpPath +import org.jdesktop.swingx.JXBusyLabel +import org.slf4j.LoggerFactory +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Desktop +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.StringSelection +import java.awt.dnd.DnDConstants +import java.awt.dnd.DropTarget +import java.awt.dnd.DropTargetDropEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.io.File +import java.nio.file.* +import javax.swing.* +import javax.swing.table.DefaultTableCellRenderer +import kotlin.io.path.exists +import kotlin.io.path.isDirectory + + +/** + * 文件系统面板 + */ +class FileSystemPanel( + private val fileSystem: FileSystem, + private val transportManager: TransportManager, + private val host: Host +) : JPanel(BorderLayout()), Disposable, + FileSystemTransportListener.Provider { + + companion object { + private val log = LoggerFactory.getLogger(FileSystemPanel::class.java) + } + + private val tableModel = FileSystemTableModel(fileSystem) + private val table = JTable(tableModel) + private val parentBtn = JButton(Icons.up) + private val workdirTextField = OutlineTextField() + private val owner get() = SwingUtilities.getWindowAncestor(this) + private val layeredPane = FileSystemLayeredPane() + private val loadingPanel = LoadingPanel() + private val bookmarkBtn = BookmarkButton() + private val homeBtn = JButton(Icons.homeFolder) + + val workdir get() = tableModel.workdir + + init { + initView() + initEvents() + } + + private fun initView() { + + // 设置书签名称 + bookmarkBtn.name = "Host.${host.id}.Bookmarks" + bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString()) + + table.autoResizeMode = JTable.AUTO_RESIZE_OFF + table.fillsViewportHeight = true + table.putClientProperty( + FlatClientProperties.STYLE, mapOf( + "showHorizontalLines" to true, + "showVerticalLines" to true, + ) + ) + + table.setDefaultRenderer( + Any::class.java, + DefaultTableCellRenderer().apply { + horizontalAlignment = SwingConstants.CENTER + } + ) + + val modifyDateColumn = table.columnModel.getColumn(FileSystemTableModel.COLUMN_LAST_MODIFIED_TIME) + modifyDateColumn.preferredWidth = 130 + + val nameColumn = table.columnModel.getColumn(FileSystemTableModel.COLUMN_NAME) + nameColumn.preferredWidth = 250 + nameColumn.setCellRenderer(object : DefaultTableCellRenderer() { + private val b = BorderFactory.createEmptyBorder(0, 4, 0, 0) + private val d = FlatFileViewDirectoryIcon() + private val f = FlatFileViewFileIcon() + + override fun getTableCellRendererComponent( + table: JTable?, + value: Any, + isSelected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int + ): Component { + var text = value.toString() + // name + if (value is FileSystemTableModel.CacheablePath) { + text = value.fileName + icon = if (value.isDirectory) d else f + iconTextGap = 4 + } + + val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column) + border = b + return c + } + }) + + parentBtn.toolTipText = I18n.getString("termora.transport.parent-folder") + + + val toolbar = FlatToolBar() + toolbar.add(homeBtn) + toolbar.add(Box.createHorizontalStrut(2)) + toolbar.add(workdirTextField) + toolbar.add(bookmarkBtn) + toolbar.add(parentBtn) + toolbar.add(JButton(Icons.refresh).apply { + addActionListener { reload() } + toolTipText = I18n.getString("termora.transport.table.contextmenu.refresh") + }) + toolbar.border = BorderFactory.createEmptyBorder(4, 2, 4, 2) + + val scrollPane = JScrollPane(table) + scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) + layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) + layeredPane.add(loadingPanel, JLayeredPane.MODAL_LAYER as Any) + + add(toolbar, BorderLayout.NORTH) + add(layeredPane, BorderLayout.CENTER) + + } + + private fun initEvents() { + + homeBtn.addActionListener { + if (tableModel.isLocalFileSystem) { + tableModel.workdir(SystemUtils.USER_HOME) + } else if (fileSystem is SftpFileSystem) { + tableModel.workdir(fileSystem.defaultDir) + } + reload() + } + + bookmarkBtn.addActionListener { e -> + if (e.actionCommand.isNullOrBlank()) { + if (bookmarkBtn.isBookmark) { + bookmarkBtn.deleteBookmark(workdir.toString()) + } else { + bookmarkBtn.addBookmark(workdir.toString()) + } + bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark + } else if (!loadingPanel.isLoading) { + tableModel.workdir(e.actionCommand) + reload() + } + } + + // contextmenu + table.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (SwingUtilities.isRightMouseButton(e)) { + val r = table.rowAtPoint(e.point) + if (r >= 0 && r < table.rowCount) { + if (!table.isRowSelected(r)) { + table.setRowSelectionInterval(r, r) + } + } else { + table.clearSelection() + } + + val rows = table.selectedRows + + if (!table.hasFocus()) { + table.requestFocusInWindow() + } + + showContextMenu(rows.filter { it != 0 }.toIntArray(), e) + } + } + }) + + + // double click + table.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { + val row = table.selectedRow + if (row < 0) return + val path = tableModel.getCacheablePath(row) + if (path.isDirectory) { + openFolder() + } else { + transport(listOf(path)) + } + } + } + }) + + // 本地文件系统不支持本地拖拽进去 + if (!tableModel.isLocalFileSystem) { + table.dropTarget = object : DropTarget() { + override fun drop(dtde: DropTargetDropEvent) { + val transportPanel = getTransportPanel() ?: return + val localFileSystemPanel = transportPanel.leftFileSystemTabbed.getFileSystemPanel(0) ?: return + + dtde.acceptDrop(DnDConstants.ACTION_COPY) + val files = dtde.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> + if (files.isEmpty()) return + + val paths = files.filterIsInstance().map { FileSystemTableModel.CacheablePath(it.toPath()) } + for (path in paths) { + if (path.isDirectory) { + Files.walk(path.path).use { + for (e in it) { + transportPanel.transport( + sourceWorkdir = path.path.parent, + targetWorkdir = workdir, + isSourceDirectory = e.isDirectory(), + sourcePath = e, + sourceHolder = localFileSystemPanel, + targetHolder = this@FileSystemPanel + ) + } + } + } else { + transportPanel.transport( + sourceWorkdir = localFileSystemPanel.workdir, + targetWorkdir = workdir, + isSourceDirectory = false, + sourcePath = path.path, + sourceHolder = localFileSystemPanel, + targetHolder = this@FileSystemPanel + ) + } + } + } + }.apply { + this.defaultActions = DnDConstants.ACTION_COPY + } + } + + // 工作目录变动 + tableModel.addPropertyChangeListener { + if (it.propertyName == "workdir") { + workdirTextField.text = tableModel.workdir.toAbsolutePath().toString() + bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdirTextField.text) + } + } + + // 修改工作目录 + workdirTextField.addActionListener { + val text = workdirTextField.text + if (text.isBlank()) { + workdirTextField.text = tableModel.workdir.toAbsolutePath().toString() + reload() + } else { + val path = fileSystem.getPath(workdirTextField.text) + if (Files.exists(path)) { + tableModel.workdir(path) + reload() + } else { + workdirTextField.outline = "error" + } + } + } + + // 返回上一级目录 + parentBtn.addActionListener { + if (tableModel.rowCount > 0) { + val path = tableModel.getCacheablePath(0) + if (path.isDirectory && path.fileName == "..") { + tableModel.workdir(path.path) + reload() + } + } + } + + + } + + + @OptIn(DelicateCoroutinesApi::class) + fun reload() { + if (loadingPanel.isLoading) { + return + } + + GlobalScope.launch(Dispatchers.IO) { + runCatching { suspendReload() } + } + } + + private suspend fun suspendReload() { + if (loadingPanel.isLoading) { + return + } + + withContext(Dispatchers.Swing) { + // reload + loadingPanel.start() + workdirTextField.text = workdir.toString() + } + + try { + tableModel.reload() + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + owner, + ExceptionUtils.getRootCauseMessage(e), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + return + } finally { + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + } + + withContext(Dispatchers.Swing) { + table.scrollRectToVisible(table.getCellRect(0, 0, true)) + } + } + + + override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { + listenerList.add(FileSystemTransportListener::class.java, listener) + } + + override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { + listenerList.remove(FileSystemTransportListener::class.java, listener) + } + + private fun openFolder() { + val row = table.selectedRow + if (row < 0) return + val path = tableModel.getCacheablePath(row) + if (path.isDirectory) { + tableModel.workdir(path.path) + reload() + } + } + + private fun canTransfer(): Boolean { + return getTransportPanel()?.getTargetFileSystemPanel(this) != null + } + + + private fun getTransportPanel(): TransportPanel? { + var p = this as Component? + while (p != null) { + if (p is TransportPanel) { + return p + } + p = p.parent + } + return null + } + + private fun showContextMenu(rows: IntArray, event: MouseEvent) { + val popupMenu = FlatPopupMenu() + val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new")) + + // 创建文件夹 + newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.folder")).addActionListener { + newFolderOrFile(file = false) + } + + // 创建文件 + newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.file")).addActionListener { + newFolderOrFile(file = true) + } + + + // 传输 + val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer")) + transfer.addActionListener { + val paths = rows.filter { it != 0 }.map { tableModel.getCacheablePath(it) } + if (paths.isNotEmpty()) { + transport(paths) + } + } + popupMenu.addSeparator() + + // 复制路径 + val copyPath = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.copy-path")) + copyPath.addActionListener { + val row = table.selectedRow + if (row > 0) { + toolkit.systemClipboard.setContents( + StringSelection( + tableModel.getPath(row).toAbsolutePath().toString() + ), null + ) + } + } + + // 如果是本地,那么支持打开本地路径 + if (tableModel.isLocalFileSystem) { + if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) { + popupMenu.add( + I18n.getString( + "termora.transport.table.contextmenu.open-in-folder", + if (SystemInfo.isMacOS) I18n.getString("termora.finder") + else if (SystemInfo.isWindows) I18n.getString("termora.explorer") + else I18n.getString("termora.folder") + ) + ).addActionListener { + val row = table.selectedRow + if (row > 0) { + Desktop.getDesktop().browseFileDirectory(tableModel.getPath(row).toFile()) + } + } + } + + } + popupMenu.addSeparator() + + // 重命名 + val rename = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.rename")) + rename.addActionListener { renamePath(tableModel.getPath(rows.last())) } + + // 删除 + val delete = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.delete")).apply { + addActionListener { deletePaths(rows) } + } + + // rm -rf + val rmrf = popupMenu.add(JMenuItem("rm -rf", Icons.errorIntroduction)).apply { + addActionListener { + deletePaths(rows, true) + } + } + + // 只有 SFTP 可以 + if (fileSystem !is SftpFileSystem) { + rmrf.isVisible = false + } + + // 修改权限 + val permission = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.change-permissions")) + permission.isEnabled = false + + // 如果是本地系统文件,那么不允许修改权限,用户应该自己修改 + if (!tableModel.isLocalFileSystem && rows.isNotEmpty()) { + permission.isEnabled = true + permission.addActionListener { changePermissions(tableModel.getCacheablePath(rows.last())) } + } + popupMenu.addSeparator() + + // 刷新 + popupMenu.add(I18n.getString("termora.transport.table.contextmenu.refresh")) + .apply { addActionListener { reload() } } + popupMenu.addSeparator() + + // 新建 + popupMenu.add(newMenu) + + + if (rows.isEmpty()) { + transfer.isEnabled = false + rename.isEnabled = false + delete.isEnabled = false + rmrf.isEnabled = false + copyPath.isEnabled = false + permission.isEnabled = false + } else { + transfer.isEnabled = canTransfer() + } + + + popupMenu.show(table, event.x, event.y) + } + + + @OptIn(DelicateCoroutinesApi::class) + private fun renamePath(path: Path) { + val fileName = path.fileName.toString() + val text = InputDialog( + owner = owner, + title = fileName, + text = fileName, + ).getText() ?: return + + if (fileName == text) return + + loadingPanel.stop() + + GlobalScope.launch(Dispatchers.IO) { + val result = runCatching { + Files.move(path, path.parent.resolve(text), StandardCopyOption.ATOMIC_MOVE) + }.onFailure { + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + owner, it.message ?: ExceptionUtils.getRootCauseMessage(it), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + } + + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + + if (result.isSuccess) { + reload() + } + } + + } + + + @OptIn(DelicateCoroutinesApi::class) + private fun newFolderOrFile(file: Boolean = false) { + val title = I18n.getString("termora.transport.table.contextmenu.new.${if (file) "file" else "folder"}") + val text = InputDialog( + owner = owner, + title = title, + ).getText() ?: return + + if (text.isEmpty()) return + + loadingPanel.stop() + + GlobalScope.launch(Dispatchers.IO) { + val result = runCatching { + val path = workdir.resolve(text) + if (path.exists()) { + throw IllegalStateException(I18n.getString("termora.transport.file-already-exists", text)) + } + if (file) + Files.createFile(path) + else + Files.createDirectories(path) + }.onFailure { + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + owner, it.message ?: ExceptionUtils.getRootCauseMessage(it), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + } + + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + + if (result.isSuccess) { + reload() + } + } + + } + + + @OptIn(DelicateCoroutinesApi::class) + private fun deletePaths(rows: IntArray, rm: Boolean = false) { + if (OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(this), + I18n.getString(if (rm) "termora.transport.table.contextmenu.rm-warning" else "termora.transport.table.contextmenu.delete-warning"), + messageType = if (rm) JOptionPane.ERROR_MESSAGE else JOptionPane.WARNING_MESSAGE + ) != JOptionPane.YES_OPTION + ) { + return + } + + loadingPanel.start() + + GlobalScope.launch(Dispatchers.IO) { + runCatching { + for (row in rows.sortedDescending()) { + try { + deleteRecursively(tableModel.getPath(row), rm) + withContext(Dispatchers.Swing) { + tableModel.removeRow(row) + } + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + } + } + }.onFailure { + if (log.isErrorEnabled) { + log.error(it.message, it) + } + } + + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + } + } + + private fun deleteRecursively(path: Path, rm: Boolean) { + if (path.fileSystem == FileSystems.getDefault()) { + FileUtils.deleteDirectory(path.toFile()) + } else if (path.fileSystem is SftpFileSystem) { + val fs = path.fileSystem as SftpFileSystem + if (rm) { + fs.session.executeRemoteCommand("rm -rf '$path'") + } else { + fs.client.use { + deleteRecursivelySFTP(path as SftpPath, it) + } + } + } + } + + /** + * 优化删除效率,采用一个连接 + */ + private fun deleteRecursivelySFTP(path: SftpPath, sftpClient: SftpClient) { + val isDirectory = if (path.attributes != null) path.attributes.isDirectory else path.isDirectory() + if (isDirectory) { + for (e in sftpClient.readDir(path.toString())) { + if (e.filename == ".." || e.filename == ".") { + continue + } + if (e.attributes.isDirectory) { + deleteRecursivelySFTP(path.resolve(e.filename), sftpClient) + } else { + sftpClient.remove(path.resolve(e.filename).toString()) + } + } + sftpClient.rmdir(path.toString()) + } else { + sftpClient.remove(path.toString()) + } + + } + + @OptIn(DelicateCoroutinesApi::class) + private fun changePermissions(cacheablePath: FileSystemTableModel.CacheablePath) { + val dialog = PosixFilePermissionDialog( + SwingUtilities.getWindowAncestor(this), + cacheablePath.posixFilePermissions + ) + val permissions = dialog.open() ?: return + + loadingPanel.start() + + GlobalScope.launch(Dispatchers.IO) { + val result = runCatching { + Files.setPosixFilePermissions(cacheablePath.path, permissions) + } + + result.onFailure { + if (log.isErrorEnabled) { + log.error(it.message, it) + } + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + SwingUtilities.getWindowAncestor(this@FileSystemPanel), ExceptionUtils.getRootCauseMessage(it), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + } + + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + + if (result.isSuccess) { + reload() + } + + + } + } + + private fun transport(paths: List) { + assertEventDispatchThread() + if (!canTransfer()) { + return + } + + loadingPanel.start() + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + runCatching { doTransport(paths) } + withContext(Dispatchers.Swing) { + loadingPanel.stop() + } + } + } + + private suspend fun doTransport(paths: List) { + if (paths.isEmpty()) return + + val listeners = listenerList.getListeners(FileSystemTransportListener::class.java) + if (listeners.isEmpty()) return + + + // 收集数据 + for (e in paths) { + + if (!e.isDirectory) { + withContext(Dispatchers.Swing) { + listeners.forEach { it.transport(this@FileSystemPanel, workdir, false, e.path) } + } + continue + } + + withContext(Dispatchers.IO) { + Files.walk(e.path).use { walkPaths -> + for (path in walkPaths) { + if (path is SftpPath) { + val isDirectory = if (path.attributes != null) + path.attributes.isDirectory else path.isDirectory() + withContext(Dispatchers.Swing) { + listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) } + } + } else { + val isDirectory = path.isDirectory() + withContext(Dispatchers.Swing) { + listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) } + } + } + } + } + } + } + } + + private class LoadingPanel : JPanel() { + private val busyLabel = JXBusyLabel() + + val isLoading get() = busyLabel.isBusy + + init { + isOpaque = false + border = BorderFactory.createEmptyBorder(50, 0, 0, 0) + + add(busyLabel, BorderLayout.CENTER) + addMouseListener(object : MouseAdapter() {}) + isVisible = false + } + + fun start() { + busyLabel.isBusy = true + isVisible = true + } + + fun stop() { + busyLabel.isBusy = false + isVisible = false + } + } + + private class FileSystemLayeredPane : JLayeredPane() { + override fun doLayout() { + synchronized(treeLock) { + val w = width + val h = height + for (c in components) { + if (c is JScrollPane) { + c.setBounds(0, 0, w, h) + } else if (c is LoadingPanel) { + c.setBounds(0, 0, w, h) + } + } + } + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt b/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt new file mode 100644 index 0000000..9c4170a --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt @@ -0,0 +1,205 @@ +package app.termora.transport + +import app.termora.* +import com.formdev.flatlaf.extras.components.FlatTabbedPane +import org.apache.commons.lang3.StringUtils +import java.awt.Point +import java.nio.file.FileSystems +import java.nio.file.Path +import javax.swing.* +import kotlin.math.max + + +class FileSystemTabbed( + private val transportManager: TransportManager, + private val isLeft: Boolean = false +) : FlatTabbedPane(), FileSystemTransportListener.Provider, Disposable { + private val addBtn = JButton(Icons.add) + private val listeners = mutableListOf() + + init { + initView() + initEvents() + } + + private fun initView() { + tabLayoutPolicy = SCROLL_TAB_LAYOUT + isTabsClosable = true + tabType = TabType.underlined + styleMap = mapOf( + "focusColor" to UIManager.getColor("TabbedPane.selectedBackground"), + ) + + + val toolbar = JToolBar() + toolbar.add(addBtn) + trailingComponent = toolbar + + if (isLeft) { + addFileSystemTransportProvider( + I18n.getString("termora.transport.local"), + FileSystemPanel( + FileSystems.getDefault(), + transportManager, + host = Host( + id = "local", + name = I18n.getString("termora.transport.local"), + protocol = Protocol.Local, + ) + ).apply { reload() } + ) + setTabClosable(0, false) + } else { + addFileSystemTransportProvider( + I18n.getString("termora.transport.sftp.select-host"), + SftpFileSystemPanel(transportManager) + ) + } + + } + + + private fun initEvents() { + addBtn.addActionListener { + val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this)) + + dialog.location = Point( + addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2, + addBtn.locationOnScreen.y + max(tabHeight, addBtn.height) + ) + dialog.isVisible = true + + for (host in dialog.hosts) { + val panel = SftpFileSystemPanel(transportManager, host) + addFileSystemTransportProvider(host.name, panel) + panel.connect() + } + + } + + + setTabCloseCallback { _, index -> + removeTabAt(index) + } + } + + override fun removeTabAt(index: Int) { + + val fileSystemPanel = getFileSystemPanel(index) + + // 取消进行中的任务 + if (fileSystemPanel != null) { + val transports = mutableListOf() + for (transport in transportManager.getTransports()) { + if (transport.targetHolder == fileSystemPanel || transport.sourceHolder == fileSystemPanel) { + transports.add(transport) + } + } + + if (transports.isNotEmpty()) { + if (OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(this), + I18n.getString("termora.transport.sftp.close-tab"), + messageType = JOptionPane.WARNING_MESSAGE, + optionType = JOptionPane.OK_CANCEL_OPTION + ) != JOptionPane.OK_OPTION + ) { + return + } + transports.sortedBy { it.state == TransportState.Waiting } + .forEach { transportManager.removeTransport(it) } + } + } + + val c = getComponentAt(index) + if (c is Disposable) { + Disposer.dispose(c) + } + + super.removeTabAt(index) + + if (tabCount == 0) { + if (!isLeft) { + addFileSystemTransportProvider( + I18n.getString("termora.transport.sftp.select-host"), + SftpFileSystemPanel(transportManager) + ) + } + } + + + } + + fun addFileSystemTransportProvider(title: String, provider: FileSystemTransportListener.Provider) { + if (provider !is JComponent) { + throw IllegalArgumentException("Provider is not an JComponent") + } + + provider.addFileSystemTransportListener(object : FileSystemTransportListener { + override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { + listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) } + } + }) + + // 修改 Tab名称 + provider.addPropertyChangeListener("TabName") { e -> + SwingUtilities.invokeLater { + val name = StringUtils.defaultIfEmpty( + e.newValue.toString(), + I18n.getString("termora.transport.sftp.select-host") + ) + for (i in 0 until tabCount) { + if (getComponentAt(i) == provider) { + setTitleAt(i, name) + break + } + } + } + } + + addTab(title, provider) + + if (tabCount > 0) + selectedIndex = tabCount - 1 + } + + fun getSelectedFileSystemPanel(): FileSystemPanel? { + return getFileSystemPanel(selectedIndex) + } + + fun getFileSystemPanel(index: Int): FileSystemPanel? { + if (index < 0) return null + val c = getComponentAt(index) + if (c is SftpFileSystemPanel) { + val p = c.fileSystemPanel + if (p != null) { + return p + } + } + + if (c is FileSystemPanel) { + return c + } + + return null + } + + override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { + listeners.add(listener) + } + + override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { + listeners.remove(listener) + } + + override fun dispose() { + while (tabCount > 0) { + val c = getComponentAt(0) + if (c is Disposable) { + Disposer.dispose(c) + } + super.removeTabAt(0) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileSystemTableModel.kt b/src/main/kotlin/app/termora/transport/FileSystemTableModel.kt new file mode 100644 index 0000000..0cc188d --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileSystemTableModel.kt @@ -0,0 +1,232 @@ +package app.termora.transport + +import app.termora.I18n +import app.termora.formatBytes +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.SystemUtils +import org.apache.commons.lang3.time.DateFormatUtils +import org.apache.sshd.sftp.client.fs.SftpFileSystem +import org.apache.sshd.sftp.client.fs.SftpPath +import org.slf4j.LoggerFactory +import java.beans.PropertyChangeEvent +import java.beans.PropertyChangeListener +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.PosixFilePermission +import java.nio.file.attribute.PosixFilePermissions +import java.util.* +import javax.swing.SwingUtilities +import javax.swing.table.DefaultTableModel +import kotlin.io.path.* + + +class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableModel() { + + + companion object { + const val COLUMN_NAME = 0 + const val COLUMN_TYPE = 1 + const val COLUMN_FILE_SIZE = 2 + const val COLUMN_LAST_MODIFIED_TIME = 3 + const val COLUMN_ATTRS = 4 + const val COLUMN_OWNER = 5 + } + + private val root = fileSystem.rootDirectories.first() + + var workdir: Path = if (fileSystem is SftpFileSystem) fileSystem.defaultDir + else fileSystem.getPath(SystemUtils.USER_HOME) + private set + + @Volatile + private var files: MutableList? = null + private val propertyChangeListeners = mutableListOf() + + val isLocalFileSystem by lazy { FileSystems.getDefault() == fileSystem } + + override fun getRowCount(): Int { + return files?.size ?: 0 + } + + override fun getValueAt(row: Int, column: Int): Any { + val path = files?.get(row) ?: return StringUtils.EMPTY + + if (path.fileName == ".." && column != 0) { + return StringUtils.EMPTY + } + + return try { + when (column) { + COLUMN_NAME -> path + COLUMN_FILE_SIZE -> if (path.isDirectory) StringUtils.EMPTY else formatBytes(path.fileSize) + COLUMN_TYPE -> if (path.isDirectory) I18n.getString("termora.transport.table.type.folder") else path.extension + COLUMN_LAST_MODIFIED_TIME -> DateFormatUtils.format(Date(path.lastModifiedTime), "yyyy/MM/dd HH:mm") + + // 如果是本地的并且还是Windows系统 + COLUMN_ATTRS -> if (isLocalFileSystem && SystemUtils.IS_OS_WINDOWS) StringUtils.EMPTY else PosixFilePermissions.toString( + path.posixFilePermissions + ) + + COLUMN_OWNER -> path.owner + else -> StringUtils.EMPTY + } + } catch (e: Exception) { + StringUtils.EMPTY + } + } + + override fun getColumnCount(): Int { + return 6 + } + + override fun getColumnName(column: Int): String { + return when (column) { + COLUMN_NAME -> I18n.getString("termora.transport.table.filename") + COLUMN_FILE_SIZE -> I18n.getString("termora.transport.table.size") + COLUMN_TYPE -> I18n.getString("termora.transport.table.type") + COLUMN_LAST_MODIFIED_TIME -> I18n.getString("termora.transport.table.modified-time") + COLUMN_ATTRS -> I18n.getString("termora.transport.table.permissions") + COLUMN_OWNER -> I18n.getString("termora.transport.table.owner") + else -> StringUtils.EMPTY + } + } + + fun getPath(index: Int): Path { + return getCacheablePath(index).path + } + + fun getCacheablePath(index: Int): CacheablePath { + return files?.get(index) ?: throw IndexOutOfBoundsException() + } + + override fun isCellEditable(row: Int, column: Int): Boolean { + return false + } + + override fun removeRow(row: Int) { + files?.removeAt(row) ?: return + fireTableRowsDeleted(row, row) + } + + fun reload() { + val files = mutableListOf() + if (root != workdir) { + files.add(CacheablePath(workdir.resolve(".."))) + } + + Files.list(workdir).use { + for (path in it) { + if (path is SftpPath) { + files.add(SftpCacheablePath(path)) + } else { + files.add(CacheablePath(path)) + } + } + } + files.sortWith(compareBy({ !it.isDirectory }, { it.fileName })) + + SwingUtilities.invokeLater { + this.files = files + fireTableDataChanged() + } + } + + fun workdir(absolutePath: String) { + workdir(fileSystem.getPath(absolutePath)) + } + + fun workdir(path: Path) { + this.workdir = path.toAbsolutePath().normalize() + propertyChangeListeners.forEach { + it.propertyChange( + PropertyChangeEvent( + this, + "workdir", + this.workdir, + this.workdir + ) + ) + } + } + + fun addPropertyChangeListener(propertyChangeListener: PropertyChangeListener) { + propertyChangeListeners.add(propertyChangeListener) + } + + open class CacheablePath(val path: Path) { + val fileName by lazy { path.fileName.toString() } + val extension by lazy { path.extension } + + open val isDirectory by lazy { path.isDirectory() } + open val fileSize by lazy { path.fileSize() } + open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() } + open val owner by lazy { path.getOwner().toString() } + open val posixFilePermissions by lazy { + kotlin.runCatching { path.getPosixFilePermissions() }.getOrElse { emptySet() } + } + } + + class SftpCacheablePath(sftpPath: SftpPath) : CacheablePath(sftpPath) { + private val attributes = sftpPath.attributes + + companion object { + private val log = LoggerFactory.getLogger(SftpCacheablePath::class.java) + private fun fromSftpPermissions(sftpPermissions: Int): Set { + val result = mutableSetOf() + + // 将十进制权限转换为八进制字符串 + val octalPermissions = sftpPermissions.toString(8) + + // 仅取后三位权限部分 + if (octalPermissions.length < 3) { + if (log.isErrorEnabled) { + log.error("Invalid permission value: {}", sftpPermissions) + return result + } + } + + val permissionBits = octalPermissions.takeLast(3) + + // 解析每一部分的权限 + val owner = permissionBits[0].digitToInt() + val group = permissionBits[1].digitToInt() + val others = permissionBits[2].digitToInt() + + // 处理所有者权限 + if ((owner and 4) != 0) result.add(PosixFilePermission.OWNER_READ) + if ((owner and 2) != 0) result.add(PosixFilePermission.OWNER_WRITE) + if ((owner and 1) != 0) result.add(PosixFilePermission.OWNER_EXECUTE) + + // 处理组权限 + if ((group and 4) != 0) result.add(PosixFilePermission.GROUP_READ) + if ((group and 2) != 0) result.add(PosixFilePermission.GROUP_WRITE) + if ((group and 1) != 0) result.add(PosixFilePermission.GROUP_EXECUTE) + + // 处理其他用户权限 + if ((others and 4) != 0) result.add(PosixFilePermission.OTHERS_READ) + if ((others and 2) != 0) result.add(PosixFilePermission.OTHERS_WRITE) + if ((others and 1) != 0) result.add(PosixFilePermission.OTHERS_EXECUTE) + + return result + } + } + + override val isDirectory: Boolean + get() = attributes.isDirectory + + override val fileSize: Long + get() = attributes.size + + override val lastModifiedTime: Long + by lazy { attributes.modifyTime.toMillis() } + + override val owner: String + get() = attributes.owner + + override val posixFilePermissions: Set + by lazy { fromSftpPermissions(attributes.permissions) } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt b/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt new file mode 100644 index 0000000..1bc414d --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt @@ -0,0 +1,19 @@ +package app.termora.transport + +import java.nio.file.Path +import java.util.* + +interface FileSystemTransportListener : EventListener { + /** + * @param workdir 当前工作目录 + * @param isDirectory 要传输的是否是文件夹 + * @param path 要传输的文件/文件夹 + */ + fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) + + + interface Provider { + fun addFileSystemTransportListener(listener: FileSystemTransportListener) + fun removeFileSystemTransportListener(listener: FileSystemTransportListener) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileTransportPanel.kt b/src/main/kotlin/app/termora/transport/FileTransportPanel.kt new file mode 100644 index 0000000..bc137b5 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileTransportPanel.kt @@ -0,0 +1,162 @@ +package app.termora.transport + +import app.termora.Disposable +import app.termora.I18n +import app.termora.OptionPane +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.extras.components.FlatPopupMenu +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Graphics +import java.awt.Insets +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.* +import javax.swing.table.DefaultTableCellRenderer + +class FileTransportPanel( + private val transportManager: TransportManager +) : JPanel(BorderLayout()), Disposable { + + private val tableModel = FileTransportTableModel(transportManager) + private val table = JTable(tableModel) + + init { + initView() + initEvents() + } + + private fun initView() { + table.fillsViewportHeight = true + table.autoResizeMode = JTable.AUTO_RESIZE_OFF + table.putClientProperty( + FlatClientProperties.STYLE, mapOf( + "showHorizontalLines" to true, + "showVerticalLines" to true, + "cellMargins" to Insets(2, 2, 2, 2) + ) + ) + table.columnModel.getColumn(FileTransportTableModel.COLUMN_NAME).preferredWidth = 200 + table.columnModel.getColumn(FileTransportTableModel.COLUMN_SOURCE_PATH).preferredWidth = 200 + table.columnModel.getColumn(FileTransportTableModel.COLUMN_TARGET_PATH).preferredWidth = 200 + + table.columnModel.getColumn(FileTransportTableModel.COLUMN_STATUS).preferredWidth = 100 + table.columnModel.getColumn(FileTransportTableModel.COLUMN_PROGRESS).preferredWidth = 150 + table.columnModel.getColumn(FileTransportTableModel.COLUMN_SIZE).preferredWidth = 140 + table.columnModel.getColumn(FileTransportTableModel.COLUMN_SPEED).preferredWidth = 80 + + val centerTableCellRenderer = DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER } + table.columnModel.getColumn(FileTransportTableModel.COLUMN_STATUS).cellRenderer = centerTableCellRenderer + table.columnModel.getColumn(FileTransportTableModel.COLUMN_SIZE).cellRenderer = centerTableCellRenderer + table.columnModel.getColumn(FileTransportTableModel.COLUMN_SPEED).cellRenderer = centerTableCellRenderer + table.columnModel.getColumn(FileTransportTableModel.COLUMN_ESTIMATED_TIME).cellRenderer = + centerTableCellRenderer + + + table.columnModel.getColumn(FileTransportTableModel.COLUMN_PROGRESS).cellRenderer = + object : DefaultTableCellRenderer() { + init { + horizontalAlignment = SwingConstants.CENTER + } + + private var lastRow = -1 + + override fun getTableCellRendererComponent( + table: JTable?, + value: Any?, + isSelected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int + ): Component { + lastRow = row + return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) + } + + override fun paintComponent(g: Graphics) { + if (lastRow != -1) { + val row = tableModel.getTransport(lastRow) + if (row.state == TransportState.Transporting) { + g.color = UIManager.getColor("textHighlight") + g.fillRect(0, 0, (width * row.progress).toInt(), height) + } + } + super.paintComponent(g) + } + } + + + add(JScrollPane(table).apply { border = BorderFactory.createEmptyBorder() }, BorderLayout.CENTER) + } + + + private fun initEvents() { + + // contextmenu + table.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (SwingUtilities.isRightMouseButton(e)) { + val r = table.rowAtPoint(e.point) + if (r >= 0 && r < table.rowCount) { + if (!table.isRowSelected(r)) { + table.setRowSelectionInterval(r, r) + } + } else { + table.clearSelection() + } + + val rows = table.selectedRows + + if (!table.hasFocus()) { + table.requestFocusInWindow() + } + + + showContextMenu(kotlin.runCatching { + rows.map { tableModel.getTransport(it) } + }.getOrElse { emptyList() }, e) + } + } + }) + } + + + private fun showContextMenu(transports: List, event: MouseEvent) { + val popupMenu = FlatPopupMenu() + + val delete = popupMenu.add(I18n.getString("termora.transport.jobs.contextmenu.delete")).apply { + addActionListener { + if (OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(this), + I18n.getString("termora.keymgr.delete-warning"), + messageType = JOptionPane.WARNING_MESSAGE + ) == JOptionPane.YES_OPTION + ) { + for (transport in transports) { + transportManager.removeTransport(transport) + } + } + } + } + + val deleteAll = popupMenu.add(I18n.getString("termora.transport.jobs.contextmenu.delete-all")) + deleteAll.addActionListener { + if (OptionPane.showConfirmDialog( + SwingUtilities.getWindowAncestor(this), + I18n.getString("termora.keymgr.delete-warning"), + messageType = JOptionPane.WARNING_MESSAGE + ) == JOptionPane.YES_OPTION + ) { + transportManager.removeAllTransports() + } + } + + if (transports.isEmpty()) { + delete.isEnabled = false + deleteAll.isEnabled = transportManager.getTransports().isNotEmpty() + } + + popupMenu.show(table, event.x, event.y) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/FileTransportTableModel.kt b/src/main/kotlin/app/termora/transport/FileTransportTableModel.kt new file mode 100644 index 0000000..0c33156 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/FileTransportTableModel.kt @@ -0,0 +1,123 @@ +package app.termora.transport + +import app.termora.I18n +import app.termora.formatBytes +import app.termora.formatSeconds +import org.apache.commons.lang3.StringUtils +import javax.swing.SwingUtilities +import javax.swing.table.DefaultTableModel + + +class FileTransportTableModel(transportManager: TransportManager) : DefaultTableModel() { + private var isInitialized = false + + private inline fun invokeLater(crossinline block: () -> Unit) { + if (SwingUtilities.isEventDispatchThread()) { + block.invoke() + } else { + SwingUtilities.invokeLater { block.invoke() } + } + } + + init { + transportManager.addTransportListener(object : TransportListener { + override fun onTransportAdded(transport: Transport) { + invokeLater { addRow(arrayOf(transport)) } + } + + override fun onTransportRemoved(transport: Transport) { + invokeLater { + val index = getDataVector().indexOfFirst { it.firstOrNull() == transport } + if (index >= 0) { + removeRow(index) + } + } + } + + override fun onTransportChanged(transport: Transport) { + invokeLater { + for ((index, vector) in getDataVector().withIndex()) { + if (vector.firstOrNull() == transport) { + fireTableRowsUpdated(index, index) + } + } + } + } + + }) + + isInitialized = true + } + + companion object { + const val COLUMN_NAME = 0 + const val COLUMN_STATUS = 1 + const val COLUMN_PROGRESS = 2 + const val COLUMN_SIZE = 3 + const val COLUMN_SOURCE_PATH = 4 + const val COLUMN_TARGET_PATH = 5 + const val COLUMN_SPEED = 6 + const val COLUMN_ESTIMATED_TIME = 7 + } + + override fun getColumnCount(): Int { + return 8 + } + + fun getTransport(row: Int): Transport { + return super.getValueAt(row, COLUMN_NAME) as Transport + } + + override fun getValueAt(row: Int, column: Int): Any { + val transport = getTransport(row) + val isTransporting = transport.state == TransportState.Transporting + val speed = if (isTransporting) transport.speed else 0 + val estimatedTime = if (isTransporting && speed > 0) + (transport.size - transport.transferredSize) / speed else 0 + + return when (column) { + COLUMN_NAME -> " ${transport.name}" + COLUMN_STATUS -> formatStatus(transport.state) + COLUMN_PROGRESS -> String.format("%.0f%%", transport.progress * 100.0) + + // 大小 + COLUMN_SIZE -> if (transport.size < 0) "-" + else if (isTransporting) "${formatBytes(transport.transferredSize)}/${formatBytes(transport.size)}" + else formatBytes(transport.size) + + COLUMN_SOURCE_PATH -> " ${transport.getSourcePath}" + COLUMN_TARGET_PATH -> " ${transport.getTargetPath}" + COLUMN_SPEED -> if (isTransporting) formatBytes(speed) else "-" + COLUMN_ESTIMATED_TIME -> if (isTransporting && speed > 0) formatSeconds(estimatedTime) else "-" + else -> StringUtils.EMPTY + } + } + + private fun formatStatus(state: TransportState): String { + return when (state) { + TransportState.Transporting -> I18n.getString("termora.transport.sftp.status.transporting") + TransportState.Waiting -> I18n.getString("termora.transport.sftp.status.waiting") + TransportState.Done -> I18n.getString("termora.transport.sftp.status.done") + TransportState.Failed -> I18n.getString("termora.transport.sftp.status.failed") + TransportState.Cancelled -> I18n.getString("termora.transport.sftp.status.cancelled") + } + } + + override fun getColumnName(column: Int): String { + return when (column) { + COLUMN_NAME -> I18n.getString("termora.transport.jobs.table.name") + COLUMN_STATUS -> I18n.getString("termora.transport.jobs.table.status") + COLUMN_PROGRESS -> I18n.getString("termora.transport.jobs.table.progress") + COLUMN_SIZE -> I18n.getString("termora.transport.jobs.table.size") + COLUMN_SOURCE_PATH -> I18n.getString("termora.transport.jobs.table.source-path") + COLUMN_TARGET_PATH -> I18n.getString("termora.transport.jobs.table.target-path") + COLUMN_SPEED -> I18n.getString("termora.transport.jobs.table.speed") + COLUMN_ESTIMATED_TIME -> I18n.getString("termora.transport.jobs.table.estimated-time") + else -> StringUtils.EMPTY + } + } + + override fun isCellEditable(row: Int, column: Int): Boolean { + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/PosixFilePermissionDialog.kt b/src/main/kotlin/app/termora/transport/PosixFilePermissionDialog.kt new file mode 100644 index 0000000..f3dfce7 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/PosixFilePermissionDialog.kt @@ -0,0 +1,148 @@ +package app.termora.transport + +import app.termora.DialogWrapper +import app.termora.I18n +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import java.awt.Dimension +import java.awt.Window +import java.nio.file.attribute.PosixFilePermission +import javax.swing.* +import kotlin.math.max + +class PosixFilePermissionDialog( + owner: Window, + private val permissions: Set +) : DialogWrapper(owner) { + + + private val ownerRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) + private val ownerWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) + private val ownerExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) + private val groupRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) + private val groupWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) + private val groupExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) + private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) + private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) + private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) + + private var isCancelled = false + + init { + isModal = true + isResizable = false + controlsVisible = false + title = I18n.getString("termora.transport.permissions") + initView() + init() + pack() + size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 300), size.height) + setLocationRelativeTo(null) + } + + private fun initView() { + ownerRead.isSelected = permissions.contains(PosixFilePermission.OWNER_READ) + ownerWrite.isSelected = permissions.contains(PosixFilePermission.OWNER_WRITE) + ownerExecute.isSelected = permissions.contains(PosixFilePermission.OWNER_EXECUTE) + groupRead.isSelected = permissions.contains(PosixFilePermission.GROUP_READ) + groupWrite.isSelected = permissions.contains(PosixFilePermission.GROUP_WRITE) + groupExecute.isSelected = permissions.contains(PosixFilePermission.GROUP_EXECUTE) + otherRead.isSelected = permissions.contains(PosixFilePermission.OTHERS_READ) + otherWrite.isSelected = permissions.contains(PosixFilePermission.OTHERS_WRITE) + otherExecute.isSelected = permissions.contains(PosixFilePermission.OTHERS_EXECUTE) + + ownerRead.isFocusable = false + ownerWrite.isFocusable = false + ownerExecute.isFocusable = false + groupRead.isFocusable = false + groupWrite.isFocusable = false + groupExecute.isFocusable = false + otherRead.isFocusable = false + otherWrite.isFocusable = false + otherExecute.isFocusable = false + } + + override fun createCenterPanel(): JComponent { + val formMargin = "7dlu" + val layout = FormLayout( + "default:grow, $formMargin, default:grow, $formMargin, default:grow", + "pref, $formMargin, pref, $formMargin, pref" + ) + + val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin") + .layout(layout).debug(true) + + builder.add("${I18n.getString("termora.transport.permissions.file-folder-permissions")}:").xyw(1, 1, 5) + + val ownerBox = Box.createVerticalBox() + ownerBox.add(ownerRead) + ownerBox.add(ownerWrite) + ownerBox.add(ownerExecute) + ownerBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.owner")) + builder.add(ownerBox).xy(1, 3) + + val groupBox = Box.createVerticalBox() + groupBox.add(groupRead) + groupBox.add(groupWrite) + groupBox.add(groupExecute) + groupBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.group")) + builder.add(groupBox).xy(3, 3) + + val otherBox = Box.createVerticalBox() + otherBox.add(otherRead) + otherBox.add(otherWrite) + otherBox.add(otherExecute) + otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others")) + builder.add(otherBox).xy(5, 3) + + return builder.build() + } + + override fun doCancelAction() { + this.isCancelled = true + super.doCancelAction() + } + + /** + * @return 返回空表示取消了 + */ + fun open(): Set? { + isModal = true + isVisible = true + + if (isCancelled) { + return null + } + + val permissions = mutableSetOf() + if (ownerRead.isSelected) { + permissions.add(PosixFilePermission.OWNER_READ) + } + if (ownerWrite.isSelected) { + permissions.add(PosixFilePermission.OWNER_WRITE) + } + if (ownerExecute.isSelected) { + permissions.add(PosixFilePermission.OWNER_EXECUTE) + } + if (groupRead.isSelected) { + permissions.add(PosixFilePermission.GROUP_READ) + } + if (groupWrite.isSelected) { + permissions.add(PosixFilePermission.GROUP_WRITE) + } + if (groupExecute.isSelected) { + permissions.add(PosixFilePermission.GROUP_EXECUTE) + } + if (otherRead.isSelected) { + permissions.add(PosixFilePermission.OTHERS_READ) + } + if (otherWrite.isSelected) { + permissions.add(PosixFilePermission.OTHERS_WRITE) + } + if (otherExecute.isSelected) { + permissions.add(PosixFilePermission.OTHERS_EXECUTE) + } + + return permissions + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt new file mode 100644 index 0000000..a32b608 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -0,0 +1,316 @@ +package app.termora.transport + +import app.termora.* +import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon +import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.exception.ExceptionUtils +import org.apache.sshd.client.SshClient +import org.apache.sshd.client.session.ClientSession +import org.apache.sshd.sftp.client.SftpClientFactory +import org.apache.sshd.sftp.client.fs.SftpFileSystem +import org.jdesktop.swingx.JXBusyLabel +import org.jdesktop.swingx.JXHyperlink +import org.slf4j.LoggerFactory +import java.awt.BorderLayout +import java.awt.CardLayout +import java.awt.event.ActionEvent +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.* + +class SftpFileSystemPanel( + private val transportManager: TransportManager, + private var host: Host? = null +) : JPanel(BorderLayout()), Disposable, + FileSystemTransportListener.Provider { + + companion object { + private val log = LoggerFactory.getLogger(SftpFileSystemPanel::class.java) + + private enum class State { + Initialized, + Connecting, + Connected, + ConnectFailed, + } + } + + @Volatile + private var state = State.Initialized + private val cardLayout = CardLayout() + private val cardPanel = JPanel(cardLayout) + + private val connectingPanel = ConnectingPanel() + private val selectHostPanel = SelectHostPanel() + private val connectFailedPanel = ConnectFailedPanel() + private val listeners = mutableListOf() + private val isDisposed = AtomicBoolean(false) + + private var client: SshClient? = null + private var session: ClientSession? = null + private var fileSystem: SftpFileSystem? = null + var fileSystemPanel: FileSystemPanel? = null + + + init { + initView() + initEvents() + } + + private fun initView() { + cardPanel.add(selectHostPanel, State.Initialized.name) + cardPanel.add(connectingPanel, State.Connecting.name) + cardPanel.add(connectFailedPanel, State.ConnectFailed.name) + cardLayout.show(cardPanel, State.Initialized.name) + add(cardPanel, BorderLayout.CENTER) + } + + private fun initEvents() { + + } + + @OptIn(DelicateCoroutinesApi::class) + fun connect() { + GlobalScope.launch(Dispatchers.IO) { + if (state != State.Connecting) { + state = State.Connecting + + withContext(Dispatchers.Swing) { + connectingPanel.start() + cardLayout.show(cardPanel, State.Connecting.name) + } + + runCatching { doConnect() }.onFailure { + if (log.isErrorEnabled) { + log.error(it.message, it) + } + withContext(Dispatchers.Swing) { + state = State.ConnectFailed + connectFailedPanel.errorLabel.text = ExceptionUtils.getRootCauseMessage(it) + cardLayout.show(cardPanel, State.ConnectFailed.name) + } + } + + withContext(Dispatchers.Swing) { + connectingPanel.stop() + } + } + + } + } + + private suspend fun doConnect() { + + val host = this.host ?: return + + closeIO() + + try { + val client = SshClients.openClient(host).apply { client = this } + val session = SshClients.openSession(host, client).apply { session = this } + fileSystem = SftpClientFactory.instance().createSftpFileSystem(session) + session.addCloseFutureListener { onClose() } + } catch (e: Exception) { + closeIO() + throw e + } + + if (isDisposed.get()) { + throw IllegalStateException("Closed") + } + + val fileSystem = this.fileSystem ?: return + + withContext(Dispatchers.Swing) { + state = State.Connected + + val fileSystemPanel = FileSystemPanel(fileSystem, transportManager, host) + fileSystemPanel.addFileSystemTransportListener(object : FileSystemTransportListener { + override fun transport( + fileSystemPanel: FileSystemPanel, + workdir: Path, + isDirectory: Boolean, + path: Path + ) { + listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) } + } + }) + + cardPanel.add(fileSystemPanel, State.Connected.name) + cardLayout.show(cardPanel, State.Connected.name) + + firePropertyChange("TabName", StringUtils.EMPTY, host.name) + + this@SftpFileSystemPanel.fileSystemPanel = fileSystemPanel + + // 立即加载 + fileSystemPanel.reload() + } + + } + + private fun onClose() { + if (isDisposed.get()) { + return + } + + SwingUtilities.invokeLater { + closeIO() + state = State.ConnectFailed + connectFailedPanel.errorLabel.text = I18n.getString("termora.transport.sftp.closed") + cardLayout.show(cardPanel, State.ConnectFailed.name) + } + } + + private fun closeIO() { + val host = host + + fileSystemPanel?.let { Disposer.dispose(it) } + fileSystemPanel = null + + runCatching { IOUtils.closeQuietly(fileSystem) } + runCatching { IOUtils.closeQuietly(session) } + runCatching { IOUtils.closeQuietly(client) } + + if (host != null && log.isInfoEnabled) { + log.info("Sftp ${host.name} is closed") + } + } + + override fun dispose() { + if (isDisposed.compareAndSet(false, true)) { + closeIO() + } + } + + private class ConnectingPanel : JPanel(BorderLayout()) { + private val busyLabel = JXBusyLabel() + + init { + initView() + } + + private fun initView() { + val formMargin = "7dlu" + val layout = FormLayout( + "default:grow, pref, default:grow", + "40dlu, pref, $formMargin, pref" + ) + + val label = JLabel(I18n.getString("termora.transport.sftp.connecting")) + label.horizontalAlignment = SwingConstants.CENTER + + busyLabel.horizontalAlignment = SwingConstants.CENTER + busyLabel.verticalAlignment = SwingConstants.CENTER + + val builder = FormBuilder.create().layout(layout).debug(false) + builder.add(busyLabel).xy(2, 2, "fill, center") + builder.add(label).xy(2, 4) + add(builder.build(), BorderLayout.CENTER) + } + + fun start() { + busyLabel.isBusy = true + } + + fun stop() { + busyLabel.isBusy = false + } + } + + private inner class ConnectFailedPanel : JPanel(BorderLayout()) { + val errorLabel = JLabel() + + init { + initView() + } + + private fun initView() { + val formMargin = "4dlu" + val layout = FormLayout( + "default:grow, pref, default:grow", + "40dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" + ) + + errorLabel.horizontalAlignment = SwingConstants.CENTER + + val builder = FormBuilder.create().layout(layout).debug(false) + builder.add(FlatOptionPaneErrorIcon()).xy(2, 2) + builder.add(errorLabel).xyw(1, 4, 3, "fill, center") + builder.add(JXHyperlink(object : AbstractAction(I18n.getString("termora.transport.sftp.retry")) { + override fun actionPerformed(e: ActionEvent) { + connect() + } + }).apply { + horizontalAlignment = SwingConstants.CENTER + verticalAlignment = SwingConstants.CENTER + isFocusable = false + }).xy(2, 6) + builder.add(JXHyperlink(object : + AbstractAction(I18n.getString("termora.transport.sftp.select-another-host")) { + override fun actionPerformed(e: ActionEvent) { + state = State.Initialized + this@SftpFileSystemPanel.firePropertyChange("TabName", StringUtils.SPACE, StringUtils.EMPTY) + cardLayout.show(cardPanel, State.Initialized.name) + } + }).apply { + horizontalAlignment = SwingConstants.CENTER + verticalAlignment = SwingConstants.CENTER + isFocusable = false + }).xy(2, 8) + add(builder.build(), BorderLayout.CENTER) + } + } + + private inner class SelectHostPanel : JPanel(BorderLayout()) { + init { + initView() + } + + private fun initView() { + val formMargin = "4dlu" + val layout = FormLayout( + "default:grow, pref, default:grow", + "40dlu, pref, $formMargin, pref, $formMargin, pref" + ) + + + val errorInfo = JLabel(I18n.getString("termora.transport.sftp.connect-a-host")) + errorInfo.horizontalAlignment = SwingConstants.CENTER + + val builder = FormBuilder.create().layout(layout).debug(false) + builder.add(FlatOptionPaneInformationIcon()).xy(2, 2) + builder.add(errorInfo).xyw(1, 4, 3, "fill, center") + builder.add(JXHyperlink(object : AbstractAction(I18n.getString("termora.transport.sftp.select-host")) { + override fun actionPerformed(e: ActionEvent) { + val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel)) + dialog.allowMulti = false + dialog.setLocationRelativeTo(this@SelectHostPanel) + dialog.isVisible = true + this@SftpFileSystemPanel.host = dialog.hosts.firstOrNull() ?: return + connect() + } + }).apply { + horizontalAlignment = SwingConstants.CENTER + verticalAlignment = SwingConstants.CENTER + isFocusable = false + }).xy(2, 6) + add(builder.build(), BorderLayout.CENTER) + } + } + + + override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { + listeners.add(listener) + } + + override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { + listeners.remove(listener) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/Transport.kt b/src/main/kotlin/app/termora/transport/Transport.kt new file mode 100644 index 0000000..4109b8a --- /dev/null +++ b/src/main/kotlin/app/termora/transport/Transport.kt @@ -0,0 +1,274 @@ +package app.termora.transport + +import app.termora.Disposable +import org.apache.commons.io.IOUtils +import org.apache.commons.net.io.CopyStreamEvent +import org.apache.commons.net.io.CopyStreamListener +import org.apache.commons.net.io.Util +import org.apache.sshd.sftp.client.fs.SftpFileSystem +import org.eclipse.jgit.internal.transport.sshd.JGitClientSession +import org.slf4j.LoggerFactory +import java.nio.file.FileSystem +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.TimeUnit +import kotlin.io.path.exists + +enum class TransportState { + Waiting, + Transporting, + Done, + Failed, + Cancelled, +} + +abstract class Transport( + val name: String, + // 源路径 + val source: Path, + // 目标路径 + val target: Path, + val sourceHolder: Disposable, + val targetHolder: Disposable, +) : Disposable, Runnable { + + private val listeners = ArrayList() + + @Volatile + var state = TransportState.Waiting + protected set(value) { + field = value + listeners.forEach { it.onTransportChanged(this) } + } + + // 0 - 1 + var progress = 0.0 + protected set(value) { + field = value + listeners.forEach { it.onTransportChanged(this) } + } + + /** + * 要传输的大小 + */ + var size = -1L + protected set + + /** + * 已经传输的大小 + */ + var transferredSize = 0L + protected set + + /** + * 传输速度 + */ + open val speed get() = 0L + + open val getSourcePath by lazy { + getFileSystemName(source.fileSystem) + ":" + source.toAbsolutePath().normalize().toString() + } + open val getTargetPath by lazy { + getFileSystemName(target.fileSystem) + ":" + target.toAbsolutePath().normalize().toString() + } + + + fun addTransportListener(listener: TransportListener) { + listeners.add(listener) + } + + fun removeTransportListener(listener: TransportListener) { + listeners.remove(listener) + } + + override fun run() { + if (state != TransportState.Waiting) { + throw IllegalStateException("$name has already been started") + } + + state = TransportState.Transporting + } + + open fun stop() { + if (state == TransportState.Waiting || state == TransportState.Transporting) { + state = TransportState.Cancelled + } + } + + private fun getFileSystemName(fileSystem: FileSystem): String { + if (fileSystem is SftpFileSystem) { + val clientSession = fileSystem.session + if (clientSession is JGitClientSession) { + return clientSession.hostConfigEntry.host + } + } + return "file" + } +} + +private class SlidingWindowByteCounter { + private val events = ConcurrentLinkedQueue>() + private val oneSecondInMillis = TimeUnit.SECONDS.toMillis(1) + + fun addBytes(bytes: Long, time: Long) { + + // 添加当前事件 + events.add(time to bytes) + + // 移除过期事件(超过 1 秒的记录) + while (events.isNotEmpty() && events.peek().first < time - oneSecondInMillis) { + events.poll() + } + + } + + fun getLastSecondBytes(): Long { + val currentTime = System.currentTimeMillis() + + // 累加最近 1 秒内的字节数 + return events.filter { it.first >= currentTime - oneSecondInMillis } + .sumOf { it.second } + } + + fun clear() { + events.clear() + } +} + + +/** + * 文件传输 + */ +class FileTransport( + name: String, source: Path, target: Path, + sourceHolder: Disposable, targetHolder: Disposable, +) : Transport( + name, source, target, sourceHolder, targetHolder, +), CopyStreamListener { + + companion object { + private val log = LoggerFactory.getLogger(FileTransport::class.java) + } + + private var lastVisitTime = 0L + private val input by lazy { Files.newInputStream(source) } + private val output by lazy { Files.newOutputStream(target) } + private val counter = SlidingWindowByteCounter() + + override val speed: Long + get() = counter.getLastSecondBytes() + + + override fun run() { + + try { + super.run() + doTransport() + state = TransportState.Done + } catch (e: Exception) { + if (state == TransportState.Cancelled) { + if (log.isWarnEnabled) { + log.warn("Transport $name is canceled") + } + return + } + if (log.isErrorEnabled) { + log.error(e.message, e) + } + state = TransportState.Failed + } finally { + counter.clear() + } + + } + + override fun stop() { + + // 如果在传输中,那么直接关闭流 + if (state == TransportState.Transporting) { + runCatching { IOUtils.closeQuietly(input) } + runCatching { IOUtils.closeQuietly(output) } + } + + super.stop() + + counter.clear() + } + + private fun doTransport() { + size = Files.size(source) + try { + Util.copyStream( + input, + output, + Util.DEFAULT_COPY_BUFFER_SIZE * 8, + size, + this + ) + } finally { + IOUtils.closeQuietly(input, output) + } + } + + override fun bytesTransferred(event: CopyStreamEvent?) { + throw UnsupportedOperationException() + } + + override fun bytesTransferred(totalBytesTransferred: Long, bytesTransferred: Int, streamSize: Long) { + + if (state == TransportState.Cancelled) { + throw IllegalStateException("$name has already been cancelled") + } + + val now = System.currentTimeMillis() + val progress = totalBytesTransferred * 1.0 / streamSize + + counter.addBytes(bytesTransferred.toLong(), now) + + if (now - lastVisitTime < 750) { + if (progress < 1.0) { + return + } + } + + this.transferredSize = totalBytesTransferred + this.progress = progress + lastVisitTime = now + } +} + +/** + * 创建文件夹 + */ +class DirectoryTransport( + name: String, source: Path, target: Path, + sourceHolder: Disposable, + targetHolder: Disposable, +) : Transport(name, source, target, sourceHolder, targetHolder) { + companion object { + private val log = LoggerFactory.getLogger(DirectoryTransport::class.java) + } + + + override fun run() { + + try { + super.run() + if (!target.exists()) { + Files.createDirectory(target) + } + state = TransportState.Done + } catch (e: FileAlreadyExistsException) { + if (log.isWarnEnabled) { + log.warn("Directory $name already exists") + } + state = TransportState.Done + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + state = TransportState.Failed + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/TransportListener.kt b/src/main/kotlin/app/termora/transport/TransportListener.kt new file mode 100644 index 0000000..735ccda --- /dev/null +++ b/src/main/kotlin/app/termora/transport/TransportListener.kt @@ -0,0 +1,20 @@ +package app.termora.transport + +import java.util.* + +interface TransportListener : EventListener { + /** + * Added + */ + fun onTransportAdded(transport: Transport) + + /** + * Removed + */ + fun onTransportRemoved(transport: Transport) + + /** + * 状态变化 + */ + fun onTransportChanged(transport: Transport) +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/TransportManager.kt b/src/main/kotlin/app/termora/transport/TransportManager.kt new file mode 100644 index 0000000..beec91c --- /dev/null +++ b/src/main/kotlin/app/termora/transport/TransportManager.kt @@ -0,0 +1,129 @@ +package app.termora.transport + +import app.termora.Disposable +import kotlinx.coroutines.* +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds + +class TransportManager : Disposable { + private val transports = Collections.synchronizedList(mutableListOf()) + private val coroutineScope by lazy { CoroutineScope(Dispatchers.IO) } + private val isProcessing = AtomicBoolean(false) + private val listeners = mutableListOf() + private val listener = object : TransportListener { + override fun onTransportAdded(transport: Transport) { + listeners.forEach { it.onTransportAdded(transport) } + } + + override fun onTransportRemoved(transport: Transport) { + listeners.forEach { it.onTransportRemoved(transport) } + } + + override fun onTransportChanged(transport: Transport) { + listeners.forEach { it.onTransportChanged(transport) } + } + + } + + companion object { + private val log = LoggerFactory.getLogger(TransportManager::class.java) + } + + fun getTransports(): List = transports + + fun addTransport(transport: Transport) { + synchronized(transports) { + transport.addTransportListener(listener) + if (transports.add(transport)) { + listeners.forEach { it.onTransportAdded(transport) } + if (isProcessing.compareAndSet(false, true)) { + coroutineScope.launch(Dispatchers.IO) { process() } + } + } + } + } + + fun removeTransport(transport: Transport) { + synchronized(transports) { + transport.stop() + if (transports.remove(transport)) { + listeners.forEach { it.onTransportRemoved(transport) } + } + } + } + + fun removeAllTransports() { + synchronized(transports) { + while (transports.isNotEmpty()) { + removeTransport(transports.last()) + } + } + } + + fun addTransportListener(listener: TransportListener) { + listeners.add(listener) + } + + fun removeTransportListener(listener: TransportListener) { + listeners.remove(listener) + } + + private suspend fun process() { + var needDelay = false + while (coroutineScope.isActive) { + try { + + // 如果为空或者其中一个正在传输中那么挑过 + if (needDelay || transports.isEmpty()) { + needDelay = false + delay(250.milliseconds) + continue + } + + val transport = synchronized(transports) { + var transport: Transport? = null + for (e in transports) { + if (e.state != TransportState.Waiting) { + continue + } + + // 遇到传输中,那么直接跳过 + if (e.state == TransportState.Transporting) { + needDelay = true + break + } + + transport = e + break + } + return@synchronized transport + } + + if (transport == null) { + continue + } + + transport.run() + + // 成功之后 删除 + if (transport.state == TransportState.Done) { + // remove + removeTransport(transport) + } + + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + } + } + } + + + override fun dispose() { + transports.clear() + coroutineScope.cancel() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/TransportPanel.kt b/src/main/kotlin/app/termora/transport/TransportPanel.kt new file mode 100644 index 0000000..1355a06 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/TransportPanel.kt @@ -0,0 +1,194 @@ +package app.termora.transport + +import app.termora.Disposable +import app.termora.Disposer +import app.termora.DynamicColor +import app.termora.assertEventDispatchThread +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import java.awt.BorderLayout +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.io.File +import java.nio.file.Path +import javax.swing.BorderFactory +import javax.swing.JPanel +import javax.swing.JSplitPane + +/** + * 传输面板 + */ +class TransportPanel : JPanel(BorderLayout()), Disposable { + + companion object { + private val log = LoggerFactory.getLogger(TransportPanel::class.java) + } + + val transportManager = TransportManager() + + val leftFileSystemTabbed = FileSystemTabbed(transportManager, true) + val rightFileSystemTabbed = FileSystemTabbed(transportManager, false) + + private val fileTransportPanel = FileTransportPanel(transportManager) + + init { + initView() + initEvents() + } + + private fun initView() { + + Disposer.register(this, transportManager) + Disposer.register(this, leftFileSystemTabbed) + Disposer.register(this, rightFileSystemTabbed) + Disposer.register(this, fileTransportPanel) + + leftFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor) + rightFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor) + + + val splitPane = JSplitPane() + splitPane.leftComponent = leftFileSystemTabbed + splitPane.rightComponent = rightFileSystemTabbed + splitPane.resizeWeight = 0.5 + splitPane.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor) + splitPane.addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + removeComponentListener(this) + splitPane.setDividerLocation(splitPane.resizeWeight) + } + }) + + fileTransportPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) + + val rootSplitPane = JSplitPane() + rootSplitPane.orientation = JSplitPane.VERTICAL_SPLIT + rootSplitPane.topComponent = splitPane + rootSplitPane.bottomComponent = fileTransportPanel + rootSplitPane.resizeWeight = 0.75 + rootSplitPane.addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + removeComponentListener(this) + rootSplitPane.setDividerLocation(rootSplitPane.resizeWeight) + } + }) + + add(rootSplitPane, BorderLayout.CENTER) + } + + @Suppress("DuplicatedCode") + private fun initEvents() { + transportManager.addTransportListener(object : TransportListener { + override fun onTransportAdded(transport: Transport) { + } + + override fun onTransportRemoved(transport: Transport) { + + } + + override fun onTransportChanged(transport: Transport) { + if (transport.state == TransportState.Done) { + val targetHolder = transport.targetHolder + if (targetHolder is FileSystemPanel) { + if (transport.target.parent == targetHolder.workdir) { + targetHolder.reload() + } + } + } + } + + }) + + + leftFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener { + override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { + val target = rightFileSystemTabbed.getSelectedFileSystemPanel() ?: return + transport( + fileSystemPanel.workdir, target.workdir, + isSourceDirectory = isDirectory, + sourcePath = path, + sourceHolder = fileSystemPanel, + targetHolder = target, + ) + } + }) + + + rightFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener { + override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { + val target = leftFileSystemTabbed.getSelectedFileSystemPanel() ?: return + transport( + fileSystemPanel.workdir, target.workdir, + isSourceDirectory = isDirectory, + sourcePath = path, + sourceHolder = fileSystemPanel, + targetHolder = target, + ) + } + }) + } + + + fun getTargetFileSystemPanel(fileSystemPanel: FileSystemPanel): FileSystemPanel? { + + assertEventDispatchThread() + + for (i in 0 until leftFileSystemTabbed.tabCount) { + if (leftFileSystemTabbed.getFileSystemPanel(i) == fileSystemPanel) { + return rightFileSystemTabbed.getSelectedFileSystemPanel() + } + } + + for (i in 0 until rightFileSystemTabbed.tabCount) { + if (rightFileSystemTabbed.getFileSystemPanel(i) == fileSystemPanel) { + return leftFileSystemTabbed.getSelectedFileSystemPanel() + } + } + + return null + } + + fun transport( + sourceWorkdir: Path, + targetWorkdir: Path, + isSourceDirectory: Boolean, + sourcePath: Path, + sourceHolder: Disposable, + targetHolder: Disposable + ) { + val relativizePath = sourceWorkdir.relativize(sourcePath).toString() + if (StringUtils.isEmpty(relativizePath) || relativizePath == File.separator || + relativizePath == sourceWorkdir.fileSystem.separator || + relativizePath == targetWorkdir.fileSystem.separator + ) { + return + } + + val transport: Transport + if (isSourceDirectory) { + transport = DirectoryTransport( + name = sourcePath.fileName.toString(), + source = sourcePath, + target = targetWorkdir.resolve(relativizePath), + sourceHolder = sourceHolder, + targetHolder = targetHolder, + ) + } else { + transport = FileTransport( + name = sourcePath.fileName.toString(), + source = sourcePath, + target = targetWorkdir.resolve(relativizePath), + sourceHolder = sourceHolder, + targetHolder = targetHolder, + ) + } + + transportManager.addTransport(transport) + } + + override fun dispose() { + if (log.isInfoEnabled) { + log.info("Transport is disposed") + } + } +} \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 2fe7166..556df36 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -8,6 +8,9 @@ termora.remove=Delete termora.yes=Yes termora.no=No termora.date-format=MM/dd/yyyy hh:mm:ss a +termora.finder=Finder +termora.folder=Folder +termora.explorer=Explorer # update termora.update.title=New version @@ -106,7 +109,7 @@ termora.welcome.contextmenu.rename=Rename termora.welcome.contextmenu.expand-all=Expand all termora.welcome.contextmenu.collapse-all=Collapse all termora.welcome.contextmenu.new=New -termora.welcome.contextmenu.new.folder=Folder +termora.welcome.contextmenu.new.folder=${termora.folder} termora.welcome.contextmenu.new.host=Host termora.welcome.contextmenu.new.folder.name=New Folder termora.welcome.contextmenu.property=Properties @@ -187,12 +190,77 @@ termora.macro.playback=Playback termora.macro.manager=Manage Macros termora.macro.run=Run - - - # Tools termora.tools.multiple=Send commands to multiple sessions +# Transport +termora.transport.local=Local +termora.transport.parent-folder=Parent Folder +termora.transport.file-already-exists=The file {0} already exists + +termora.transport.bookmarks=Bookmarks Manager +termora.transport.bookmarks.up=Up +termora.transport.bookmarks.down=Down + +termora.transport.table.filename=Filename +termora.transport.table.type=Type +termora.transport.table.type.folder=${termora.welcome.contextmenu.new.folder} +termora.transport.table.size=Size +termora.transport.table.modified-time=Modified +termora.transport.table.permissions=Permissions +termora.transport.table.owner=Owner + +# contextmenu +termora.transport.table.contextmenu.transfer=Transfer +termora.transport.table.contextmenu.copy-path=Copy Path +termora.transport.table.contextmenu.open-in-folder=Open in {0} +termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename} +termora.transport.table.contextmenu.delete=${termora.remove} +termora.transport.table.contextmenu.delete-warning=If the folder is too large, deleting it may take some time +termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a folder is very dangerous +termora.transport.table.contextmenu.change-permissions=Change Permissions... +termora.transport.table.contextmenu.refresh=Refresh +termora.transport.table.contextmenu.new=${termora.welcome.contextmenu.new} +termora.transport.table.contextmenu.new.folder=${termora.welcome.contextmenu.new.folder.name} +termora.transport.table.contextmenu.new.file=New File + +# Permission +termora.transport.permissions=Change Permissions +termora.transport.permissions.file-folder-permissions=File/Folder Permissions +termora.transport.permissions.read=Read +termora.transport.permissions.write=Write +termora.transport.permissions.execute=Execute +termora.transport.permissions.owner=Owner +termora.transport.permissions.group=Group +termora.transport.permissions.others=Others + +termora.transport.sftp.retry=Retry +termora.transport.sftp.select-another-host=Select another host +termora.transport.sftp.select-host=Select host +termora.transport.sftp.connect-a-host=Connect to a Host +termora.transport.sftp.connecting=Connecting... +termora.transport.sftp.closed=The connection has been closed +termora.transport.sftp.close-tab=Transfer is still in activated status. Are you sure you want to remove all jobs and close this session? +termora.transport.sftp.status.transporting=Transporting +termora.transport.sftp.status.waiting=Waiting +termora.transport.sftp.status.done=Done +termora.transport.sftp.status.failed=Failed +termora.transport.sftp.status.cancelled=Cancelled + + +# transport job +termora.transport.jobs.table.name=Name +termora.transport.jobs.table.status=Status +termora.transport.jobs.table.progress=Progress +termora.transport.jobs.table.size=Size +termora.transport.jobs.table.source-path=Source Path +termora.transport.jobs.table.target-path=Target Path +termora.transport.jobs.table.speed=Speed +termora.transport.jobs.table.estimated-time=Estimated time + +termora.transport.jobs.contextmenu.delete=${termora.remove} +termora.transport.jobs.contextmenu.delete-all=Delete All + # Terminal termora.terminal.size=Size: {0} x {1} diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index ab2fccb..4f7a425 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -7,6 +7,9 @@ termora.remove=删除 termora.yes=是 termora.no=否 termora.date-format=yyyy-MM-dd HH:mm:ss +termora.finder=访达 +termora.folder=文件夹 +termora.explorer=文件管理器 # update termora.update.title=新版本 @@ -184,6 +187,70 @@ termora.macro.manager=管理宏 termora.macro.run=运行 + +# Transport +termora.transport.local=本机 +termora.transport.parent-folder=父文件夹 +termora.transport.file-already-exists=文件 {0} 已存在 + +termora.transport.bookmarks=书签管理 +termora.transport.bookmarks.up=上移 +termora.transport.bookmarks.down=下移 + +termora.transport.table.filename=文件名 +termora.transport.table.type=类型 +termora.transport.table.size=大小 +termora.transport.table.modified-time=修改时间 +termora.transport.table.permissions=权限 +termora.transport.table.owner=所有者 + +# contextmenu +termora.transport.table.contextmenu.transfer=传输 +termora.transport.table.contextmenu.copy-path=复制路径 +termora.transport.table.contextmenu.open-in-folder=在{0}中打开 +termora.transport.table.contextmenu.change-permissions=更改权限... +termora.transport.table.contextmenu.refresh=刷新 +termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件 +termora.transport.table.contextmenu.delete-warning=如果文件夹太大,删除可能需要耗费一定时间 +termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令删除文件夹存在很大风险 + +termora.transport.sftp.retry=重试 +termora.transport.sftp.select-another-host=选择其他主机 +termora.transport.sftp.select-host=选择主机 +termora.transport.sftp.connect-a-host=连接一个主机 +termora.transport.sftp.connecting=连接中... +termora.transport.sftp.closed=连接已经关闭 +termora.transport.sftp.close-tab=传输还处于活动状态,是否删除所有传输任务并关闭此会话? + +termora.transport.sftp.status.transporting=传输中 +termora.transport.sftp.status.waiting=等待中 +termora.transport.sftp.status.done=已完成 +termora.transport.sftp.status.failed=已失败 +termora.transport.sftp.status.cancelled=已取消 + + +# Permission +termora.transport.permissions=更改权限 +termora.transport.permissions.file-folder-permissions=文件/文件夹权限 +termora.transport.permissions.read=读取 +termora.transport.permissions.write=写入 +termora.transport.permissions.execute=执行 +termora.transport.permissions.owner=所有者 +termora.transport.permissions.group=组 +termora.transport.permissions.others=其他 + +# transport job +termora.transport.jobs.table.name=名称 +termora.transport.jobs.table.status=状态 +termora.transport.jobs.table.progress=进度 +termora.transport.jobs.table.size=大小 +termora.transport.jobs.table.source-path=源路径 +termora.transport.jobs.table.target-path=目标路径 +termora.transport.jobs.table.speed=速度 +termora.transport.jobs.table.estimated-time=剩余时间 + +termora.transport.jobs.contextmenu.delete-all=删除所有 + termora.terminal.size=大小: {0} x {1} termora.terminal.copied=已复制 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index e1ea859..b3903a4 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -6,6 +6,9 @@ termora.remove=刪除 termora.yes=是 termora.no=否 termora.date-format=yyyy/MM/dd HH:mm:ss +termora.finder=訪達 +termora.folder=資料夾 +termora.explorer=檔案管理器 # update termora.update.title=新版本 @@ -95,7 +98,7 @@ termora.welcome.contextmenu.rename=重新命名 termora.welcome.contextmenu.expand-all=展開全部 termora.welcome.contextmenu.collapse-all=全部收縮 termora.welcome.contextmenu.new=新建 -termora.welcome.contextmenu.new.folder=資料夾 +termora.welcome.contextmenu.new.folder=${termora.folder} termora.welcome.contextmenu.new.host=主機 termora.welcome.contextmenu.new.folder.name=新建資料夾 termora.welcome.contextmenu.property=屬性 @@ -177,6 +180,57 @@ termora.macro.playback=回放 termora.macro.manager=管理宏 termora.macro.run=運行 +# Transport +termora.transport.local=本機 +termora.transport.parent-folder=父資料夾 +termora.transport.file-already-exists=檔案 {0} 已存在 + +termora.transport.bookmarks=書籤管理 +termora.transport.bookmarks.up=上移 +termora.transport.bookmarks.down=下移 + +termora.transport.table.filename=檔名 +termora.transport.table.type=類型 +termora.transport.table.size=大小 +termora.transport.table.modified-time=修改時間 +termora.transport.table.permissions=權限 +termora.transport.table.owner=所有者 + +# contextmenu +termora.transport.table.contextmenu.transfer=傳輸 +termora.transport.table.contextmenu.copy-path=複製路徑 +termora.transport.table.contextmenu.open-in-folder=在{0}中打開 +termora.transport.table.contextmenu.change-permissions=更改權限... +termora.transport.table.contextmenu.refresh=刷新 +termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件 +termora.transport.table.contextmenu.delete-warning=如果資料夾太大,刪除可能需要耗費一定時間 +termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令刪除資料夾存在很大風險 + +termora.transport.sftp.retry=重試 +termora.transport.sftp.select-another-host=選擇其他主機 +termora.transport.sftp.select-host=選擇主機 +termora.transport.sftp.connect-a-host=連接一個主機 +termora.transport.sftp.connecting=連接中... +termora.transport.sftp.closed=連線已經關閉 +termora.transport.sftp.close-tab=傳輸仍處於活動狀態,是否刪除所有傳輸任務並關閉此會話? +termora.transport.sftp.status.transporting=傳輸中 +termora.transport.sftp.status.waiting=等待中 +termora.transport.sftp.status.done=已完成 +termora.transport.sftp.status.failed=已失敗 +termora.transport.sftp.status.cancelled=已取消 + +# transport job +termora.transport.jobs.table.name=名稱 +termora.transport.jobs.table.status=狀態 +termora.transport.jobs.table.progress=進度 +termora.transport.jobs.table.size=大小 +termora.transport.jobs.table.source-path=來源路徑 +termora.transport.jobs.table.target-path=目標路徑 +termora.transport.jobs.table.speed=速度 +termora.transport.jobs.table.estimated-time=剩餘時間 + +termora.transport.jobs.contextmenu.delete-all=刪除所有 + termora.terminal.size=大小: {0} x {1} termora.terminal.copied=已複製 diff --git a/src/main/resources/icons/bookmarks.svg b/src/main/resources/icons/bookmarks.svg new file mode 100644 index 0000000..cbb7212 --- /dev/null +++ b/src/main/resources/icons/bookmarks.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/bookmarksOff.svg b/src/main/resources/icons/bookmarksOff.svg new file mode 100644 index 0000000..8c81521 --- /dev/null +++ b/src/main/resources/icons/bookmarksOff.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/resources/icons/bookmarksOff_dark.svg b/src/main/resources/icons/bookmarksOff_dark.svg new file mode 100644 index 0000000..a2dbda2 --- /dev/null +++ b/src/main/resources/icons/bookmarksOff_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/icons/bookmarks_dark.svg b/src/main/resources/icons/bookmarks_dark.svg new file mode 100644 index 0000000..6fd28df --- /dev/null +++ b/src/main/resources/icons/bookmarks_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/bulletList.svg b/src/main/resources/icons/bulletList.svg new file mode 100644 index 0000000..ed33c60 --- /dev/null +++ b/src/main/resources/icons/bulletList.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/icons/bulletList_dark.svg b/src/main/resources/icons/bulletList_dark.svg new file mode 100644 index 0000000..5340135 --- /dev/null +++ b/src/main/resources/icons/bulletList_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/icons/errorIntroduction.svg b/src/main/resources/icons/errorIntroduction.svg new file mode 100644 index 0000000..c202817 --- /dev/null +++ b/src/main/resources/icons/errorIntroduction.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/icons/errorIntroduction_dark.svg b/src/main/resources/icons/errorIntroduction_dark.svg new file mode 100644 index 0000000..841ab73 --- /dev/null +++ b/src/main/resources/icons/errorIntroduction_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/icons/fileTransfer.svg b/src/main/resources/icons/fileTransfer.svg new file mode 100644 index 0000000..ba0aaa1 --- /dev/null +++ b/src/main/resources/icons/fileTransfer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/fileTransfer_dark.svg b/src/main/resources/icons/fileTransfer_dark.svg new file mode 100644 index 0000000..999484f --- /dev/null +++ b/src/main/resources/icons/fileTransfer_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/listFiles.svg b/src/main/resources/icons/listFiles.svg new file mode 100644 index 0000000..38ee91a --- /dev/null +++ b/src/main/resources/icons/listFiles.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/listFiles_dark.svg b/src/main/resources/icons/listFiles_dark.svg new file mode 100644 index 0000000..798a2b3 --- /dev/null +++ b/src/main/resources/icons/listFiles_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/refresh.svg b/src/main/resources/icons/refresh.svg new file mode 100644 index 0000000..7a4c73d --- /dev/null +++ b/src/main/resources/icons/refresh.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/refresh_dark.svg b/src/main/resources/icons/refresh_dark.svg new file mode 100644 index 0000000..3362992 --- /dev/null +++ b/src/main/resources/icons/refresh_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/test/kotlin/app/termora/SFTPTest.kt b/src/test/kotlin/app/termora/SFTPTest.kt new file mode 100644 index 0000000..ec6e2f6 --- /dev/null +++ b/src/test/kotlin/app/termora/SFTPTest.kt @@ -0,0 +1,54 @@ +package app.termora + +import org.apache.sshd.sftp.client.impl.DefaultSftpClientFactory +import org.testcontainers.containers.GenericContainer +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class SFTPTest { + private val sftpContainer = GenericContainer("linuxserver/openssh-server") + .withEnv("PUID", "1000") + .withEnv("PGID", "1000") + .withEnv("TZ", "Etc/UTC") + .withEnv("SUDO_ACCESS", "true") + .withEnv("PASSWORD_ACCESS", "true") + .withEnv("USER_NAME", "foo") + .withEnv("USER_PASSWORD", "pass") + .withEnv("SUDO_ACCESS", "true") + .withExposedPorts(2222) + + @BeforeTest + fun setup() { + sftpContainer.start() + } + + @AfterTest + fun teardown() { + sftpContainer.stop() + } + + @Test + fun test() { + val host = Host( + name = sftpContainer.containerName, + protocol = Protocol.SSH, + host = "127.0.0.1", + port = sftpContainer.getMappedPort(2222), + username = "foo", + authentication = Authentication.No.copy(type = AuthenticationType.Password, password = "pass"), + ) + + val client = SshClients.openClient(host) + val session = SshClients.openSession(host, client) + assertTrue(session.isOpen) + + + val fileSystem = DefaultSftpClientFactory.INSTANCE.createSftpFileSystem(session) + for (path in Files.list(fileSystem.rootDirectories.first())) { + println(path) + } + } +} \ No newline at end of file