From 88f20c48989943e203a3965817d84da608d61c07 Mon Sep 17 00:00:00 2001 From: hstyi Date: Thu, 16 Jan 2025 14:59:01 +0800 Subject: [PATCH] feat: SFTP supports pasting files for upload (#87) --- .../kotlin/app/termora/SFTPTerminalTab.kt | 5 +- .../app/termora/transport/FileSystemPanel.kt | 106 +++++++++++------- .../transport/TransportDataProviders.kt | 17 +++ .../app/termora/transport/TransportPanel.kt | 58 +++++----- src/main/resources/icons/eyeClose_dark.svg | 8 +- 5 files changed, 117 insertions(+), 77 deletions(-) create mode 100644 src/main/kotlin/app/termora/transport/TransportDataProviders.kt diff --git a/src/main/kotlin/app/termora/SFTPTerminalTab.kt b/src/main/kotlin/app/termora/SFTPTerminalTab.kt index 16cf174..d04f807 100644 --- a/src/main/kotlin/app/termora/SFTPTerminalTab.kt +++ b/src/main/kotlin/app/termora/SFTPTerminalTab.kt @@ -1,5 +1,6 @@ package app.termora +import app.termora.transport.TransportDataProviders import app.termora.transport.TransportPanel import java.beans.PropertyChangeListener import javax.swing.Icon @@ -40,8 +41,8 @@ class SFTPTerminalTab : Disposable, TerminalTab { override fun canClose(): Boolean { assertEventDispatchThread() - - if (transportPanel.transportManager.getTransports().isEmpty()) { + val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true + if (transportManager.getTransports().isEmpty()) { return true } diff --git a/src/main/kotlin/app/termora/transport/FileSystemPanel.kt b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt index ba889b1..11ea148 100644 --- a/src/main/kotlin/app/termora/transport/FileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt @@ -1,6 +1,7 @@ package app.termora.transport import app.termora.* +import app.termora.actions.AnActionEvent import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatToolBar @@ -11,6 +12,7 @@ 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.StringUtils import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.sshd.sftp.client.SftpClient @@ -26,10 +28,12 @@ import java.awt.datatransfer.StringSelection import java.awt.dnd.DnDConstants import java.awt.dnd.DropTarget import java.awt.dnd.DropTargetDropEvent +import java.awt.event.ActionEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.io.File import java.nio.file.* +import java.util.* import javax.swing.* import javax.swing.table.DefaultTableCellRenderer import kotlin.io.path.exists @@ -60,7 +64,6 @@ class FileSystemPanel( private val homeBtn = JButton(Icons.homeFolder) private val showHiddenFilesBtn = JButton(Icons.eyeClose) private val properties get() = Database.getDatabase().properties - private var isShowHiddenFiles = false private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" } val workdir get() = tableModel.workdir @@ -232,39 +235,10 @@ class FileSystemPanel( 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 = path.path.parent, - targetWorkdir = workdir, - isSourceDirectory = false, - sourcePath = path.path, - sourceHolder = localFileSystemPanel, - targetHolder = this@FileSystemPanel - ) - } - } + copyLocalFileToFileSystem(files.filterIsInstance()) } }.apply { this.defaultActions = DnDConstants.ACTION_COPY @@ -307,6 +281,7 @@ class FileSystemPanel( } } + // 显示隐藏文件 showHiddenFilesBtn.addActionListener { val showHiddenFiles = tableModel.isShowHiddenFiles tableModel.isShowHiddenFiles = !showHiddenFiles @@ -317,6 +292,19 @@ class FileSystemPanel( } } + // 如果不是本地的文件系统,那么支持粘贴 + if (!tableModel.isLocalFileSystem) { + table.actionMap.put("paste", object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { + return + } + val files = (toolkit.systemClipboard.getData(DataFlavor.javaFileListFlavor) ?: return) as List<*> + copyLocalFileToFileSystem(files.filterIsInstance()) + } + }) + } + Disposer.register(this, object : Disposable { override fun dispose() { properties.putString(showHiddenFilesKey, "${tableModel.isShowHiddenFiles}") @@ -326,6 +314,40 @@ class FileSystemPanel( } + private fun copyLocalFileToFileSystem(files: List) { + val event = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) + val transportPanel = event.getData(TransportDataProviders.TransportPanel) ?: return + val leftFileSystemTabbed = event.getData(TransportDataProviders.LeftFileSystemTabbed) ?: return + val localFileSystemPanel = leftFileSystemTabbed.getFileSystemPanel(0) ?: return + + val paths = files.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 = path.path.parent, + targetWorkdir = workdir, + isSourceDirectory = false, + sourcePath = path.path, + sourceHolder = localFileSystemPanel, + targetHolder = this@FileSystemPanel + ) + } + } + } + @OptIn(DelicateCoroutinesApi::class) fun reload() { if (loadingPanel.isLoading) { @@ -393,21 +415,21 @@ class FileSystemPanel( } private fun canTransfer(): Boolean { - return getTransportPanel()?.getTargetFileSystemPanel(this) != null - } + val event = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) + val leftFileSystemTabbed = event.getData(TransportDataProviders.LeftFileSystemTabbed) ?: return false + val rightFileSystemTabbed = event.getData(TransportDataProviders.RightFileSystemTabbed) ?: return false - - private fun getTransportPanel(): TransportPanel? { - var p = this as Component? - while (p != null) { - if (p is TransportPanel) { - return p - } - p = p.parent + val parent = SwingUtilities.getAncestorOfClass(FileSystemTabbed::class.java, this) + if (parent == leftFileSystemTabbed) { + return event.getData(TransportDataProviders.RightFileSystemPanel) != null + } else if (parent == rightFileSystemTabbed) { + return event.getData(TransportDataProviders.LeftFileSystemPanel) != null } - return null + + return false } + private fun showContextMenu(rows: IntArray, event: MouseEvent) { val popupMenu = FlatPopupMenu() val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new")) diff --git a/src/main/kotlin/app/termora/transport/TransportDataProviders.kt b/src/main/kotlin/app/termora/transport/TransportDataProviders.kt new file mode 100644 index 0000000..ea42569 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/TransportDataProviders.kt @@ -0,0 +1,17 @@ +package app.termora.transport + +import app.termora.terminal.DataKey + +object TransportDataProviders { + val LeftFileSystemPanel = DataKey(FileSystemPanel::class) + val RightFileSystemPanel = DataKey(FileSystemPanel::class) + + val LeftFileSystemTabbed = DataKey(FileSystemTabbed::class) + val RightFileSystemTabbed = DataKey(FileSystemTabbed::class) + + val TransportManager = DataKey(app.termora.transport.TransportManager::class) + + val TransportPanel = DataKey(app.termora.transport.TransportPanel::class) +} + + diff --git a/src/main/kotlin/app/termora/transport/TransportPanel.kt b/src/main/kotlin/app/termora/transport/TransportPanel.kt index 1355a06..32bbbe8 100644 --- a/src/main/kotlin/app/termora/transport/TransportPanel.kt +++ b/src/main/kotlin/app/termora/transport/TransportPanel.kt @@ -3,7 +3,9 @@ package app.termora.transport import app.termora.Disposable import app.termora.Disposer import app.termora.DynamicColor -import app.termora.assertEventDispatchThread +import app.termora.actions.DataProvider +import app.termora.actions.DataProviderSupport +import app.termora.terminal.DataKey import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import java.awt.BorderLayout @@ -18,17 +20,17 @@ import javax.swing.JSplitPane /** * 传输面板 */ -class TransportPanel : JPanel(BorderLayout()), Disposable { +class TransportPanel : JPanel(BorderLayout()), Disposable, DataProvider { 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 dataProviderSupport = DataProviderSupport() + private val transportManager = TransportManager() + private val leftFileSystemTabbed = FileSystemTabbed(transportManager, true) + private val rightFileSystemTabbed = FileSystemTabbed(transportManager, false) private val fileTransportPanel = FileTransportPanel(transportManager) init { @@ -43,6 +45,11 @@ class TransportPanel : JPanel(BorderLayout()), Disposable { Disposer.register(this, rightFileSystemTabbed) Disposer.register(this, fileTransportPanel) + dataProviderSupport.addData(TransportDataProviders.LeftFileSystemTabbed, leftFileSystemTabbed) + dataProviderSupport.addData(TransportDataProviders.RightFileSystemTabbed, rightFileSystemTabbed) + dataProviderSupport.addData(TransportDataProviders.TransportManager, transportManager) + dataProviderSupport.addData(TransportDataProviders.TransportPanel, this) + leftFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor) rightFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor) @@ -128,26 +135,6 @@ class TransportPanel : JPanel(BorderLayout()), Disposable { }) } - - 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, @@ -191,4 +178,23 @@ class TransportPanel : JPanel(BorderLayout()), Disposable { log.info("Transport is disposed") } } + + override fun getData(dataKey: DataKey): T? { + if (dataKey == TransportDataProviders.LeftFileSystemPanel || + dataKey == TransportDataProviders.RightFileSystemPanel + ) { + dataProviderSupport.removeData(dataKey) + if (dataKey == TransportDataProviders.LeftFileSystemPanel) { + leftFileSystemTabbed.getSelectedFileSystemPanel()?.let { + dataProviderSupport.addData(dataKey, it) + } + } else { + rightFileSystemTabbed.getSelectedFileSystemPanel()?.let { + dataProviderSupport.addData(dataKey, it) + } + } + + } + return dataProviderSupport.getData(dataKey) + } } \ No newline at end of file diff --git a/src/main/resources/icons/eyeClose_dark.svg b/src/main/resources/icons/eyeClose_dark.svg index 3578a02..d13ce95 100644 --- a/src/main/resources/icons/eyeClose_dark.svg +++ b/src/main/resources/icons/eyeClose_dark.svg @@ -1,7 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file