feat: SFTP supports pasting files for upload (#87)

This commit is contained in:
hstyi
2025-01-16 14:59:01 +08:00
committed by GitHub
parent 314c112d4b
commit 88f20c4898
5 changed files with 117 additions and 77 deletions

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.transport.TransportDataProviders
import app.termora.transport.TransportPanel import app.termora.transport.TransportPanel
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.Icon import javax.swing.Icon
@@ -40,8 +41,8 @@ class SFTPTerminalTab : Disposable, TerminalTab {
override fun canClose(): Boolean { override fun canClose(): Boolean {
assertEventDispatchThread() assertEventDispatchThread()
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
if (transportPanel.transportManager.getTransports().isEmpty()) { if (transportManager.getTransports().isEmpty()) {
return true return true
} }

View File

@@ -1,6 +1,7 @@
package app.termora.transport package app.termora.transport
import app.termora.* import app.termora.*
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -11,6 +12,7 @@ import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sshd.sftp.client.SftpClient 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.DnDConstants
import java.awt.dnd.DropTarget import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetDropEvent import java.awt.dnd.DropTargetDropEvent
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.io.File import java.io.File
import java.nio.file.* import java.nio.file.*
import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import kotlin.io.path.exists import kotlin.io.path.exists
@@ -60,7 +64,6 @@ class FileSystemPanel(
private val homeBtn = JButton(Icons.homeFolder) private val homeBtn = JButton(Icons.homeFolder)
private val showHiddenFilesBtn = JButton(Icons.eyeClose) private val showHiddenFilesBtn = JButton(Icons.eyeClose)
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private var isShowHiddenFiles = false
private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" } private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" }
val workdir get() = tableModel.workdir val workdir get() = tableModel.workdir
@@ -232,39 +235,10 @@ class FileSystemPanel(
if (!tableModel.isLocalFileSystem) { if (!tableModel.isLocalFileSystem) {
table.dropTarget = object : DropTarget() { table.dropTarget = object : DropTarget() {
override fun drop(dtde: DropTargetDropEvent) { override fun drop(dtde: DropTargetDropEvent) {
val transportPanel = getTransportPanel() ?: return
val localFileSystemPanel = transportPanel.leftFileSystemTabbed.getFileSystemPanel(0) ?: return
dtde.acceptDrop(DnDConstants.ACTION_COPY) dtde.acceptDrop(DnDConstants.ACTION_COPY)
val files = dtde.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> val files = dtde.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
if (files.isEmpty()) return if (files.isEmpty()) return
copyLocalFileToFileSystem(files.filterIsInstance<File>())
val paths = files.filterIsInstance<File>().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
)
}
}
} }
}.apply { }.apply {
this.defaultActions = DnDConstants.ACTION_COPY this.defaultActions = DnDConstants.ACTION_COPY
@@ -307,6 +281,7 @@ class FileSystemPanel(
} }
} }
// 显示隐藏文件
showHiddenFilesBtn.addActionListener { showHiddenFilesBtn.addActionListener {
val showHiddenFiles = tableModel.isShowHiddenFiles val showHiddenFiles = tableModel.isShowHiddenFiles
tableModel.isShowHiddenFiles = !showHiddenFiles 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<File>())
}
})
}
Disposer.register(this, object : Disposable { Disposer.register(this, object : Disposable {
override fun dispose() { override fun dispose() {
properties.putString(showHiddenFilesKey, "${tableModel.isShowHiddenFiles}") properties.putString(showHiddenFilesKey, "${tableModel.isShowHiddenFiles}")
@@ -326,6 +314,40 @@ class FileSystemPanel(
} }
private fun copyLocalFileToFileSystem(files: List<File>) {
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) @OptIn(DelicateCoroutinesApi::class)
fun reload() { fun reload() {
if (loadingPanel.isLoading) { if (loadingPanel.isLoading) {
@@ -393,21 +415,21 @@ class FileSystemPanel(
} }
private fun canTransfer(): Boolean { 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
val parent = SwingUtilities.getAncestorOfClass(FileSystemTabbed::class.java, this)
private fun getTransportPanel(): TransportPanel? { if (parent == leftFileSystemTabbed) {
var p = this as Component? return event.getData(TransportDataProviders.RightFileSystemPanel) != null
while (p != null) { } else if (parent == rightFileSystemTabbed) {
if (p is TransportPanel) { return event.getData(TransportDataProviders.LeftFileSystemPanel) != null
return p
}
p = p.parent
} }
return null
return false
} }
private fun showContextMenu(rows: IntArray, event: MouseEvent) { private fun showContextMenu(rows: IntArray, event: MouseEvent) {
val popupMenu = FlatPopupMenu() val popupMenu = FlatPopupMenu()
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new")) val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))

View File

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

View File

@@ -3,7 +3,9 @@ package app.termora.transport
import app.termora.Disposable import app.termora.Disposable
import app.termora.Disposer import app.termora.Disposer
import app.termora.DynamicColor 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.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout 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 { companion object {
private val log = LoggerFactory.getLogger(TransportPanel::class.java) private val log = LoggerFactory.getLogger(TransportPanel::class.java)
} }
val transportManager = TransportManager() private val dataProviderSupport = DataProviderSupport()
val leftFileSystemTabbed = FileSystemTabbed(transportManager, true)
val rightFileSystemTabbed = FileSystemTabbed(transportManager, false)
private val transportManager = TransportManager()
private val leftFileSystemTabbed = FileSystemTabbed(transportManager, true)
private val rightFileSystemTabbed = FileSystemTabbed(transportManager, false)
private val fileTransportPanel = FileTransportPanel(transportManager) private val fileTransportPanel = FileTransportPanel(transportManager)
init { init {
@@ -43,6 +45,11 @@ class TransportPanel : JPanel(BorderLayout()), Disposable {
Disposer.register(this, rightFileSystemTabbed) Disposer.register(this, rightFileSystemTabbed)
Disposer.register(this, fileTransportPanel) 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) leftFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
rightFileSystemTabbed.border = BorderFactory.createMatteBorder(0, 1, 0, 0, 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( fun transport(
sourceWorkdir: Path, sourceWorkdir: Path,
targetWorkdir: Path, targetWorkdir: Path,
@@ -191,4 +178,23 @@ class TransportPanel : JPanel(BorderLayout()), Disposable {
log.info("Transport is disposed") log.info("Transport is disposed")
} }
} }
override fun <T : Any> getData(dataKey: DataKey<T>): 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)
}
} }

View File

@@ -1,7 +1 @@
<svg t="1736928517310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="877" <svg t="1736928586708" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="907" width="16" height="16"><path d="M1001.58577778 482.87288889l-0.11377778-0.11377778-0.11377778-0.11377778c-41.41511111-87.26755555-91.02222222-157.80977778-148.70755555-211.62666666L794.96533333 328.81777778c49.72088889 45.73866667 92.72888889 106.60977778 129.82044445 183.06844444C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555c-58.368 0-111.84355555-9.44355555-160.65422222-28.55822222l-62.23644445 62.23644445C355.66933333 866.75911111 429.85244445 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0.11377778-58.368zM928.768 104.90311111l-48.24177778-48.24177778c-3.52711111-3.52711111-9.32977778-3.52711111-12.85688889 0L734.77688889 189.44C668.33066667 157.24088889 594.14755555 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666v0.11377778c-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556 41.41511111 87.26755555 91.02222222 157.80977778 148.70755555 211.74044444L56.66133333 867.55555555c-3.52711111 3.52711111-3.52711111 9.32977778 0 12.8568889l48.24177778 48.24177777c3.52711111 3.52711111 9.32977778 3.52711111 12.85688889 0l811.008-811.008c3.52711111-3.41333333 3.52711111-9.216 0-12.74311111zM383.31733333 540.89955555c-2.16177778-9.32977778-3.29955555-19.00088889-3.29955555-28.89955555 0-70.42844445 57.00266667-127.43111111 127.43111111-127.43111111 9.89866667 0 19.68355555 1.13777778 28.89955556 3.29955556L383.31733333 540.89955555z m209.92-209.92C567.18222222 318.69155555 538.16888889 311.75111111 507.44888889 311.75111111c-110.592 0-200.24888889 89.65688889-200.24888889 200.24888889 0 30.72 6.94044445 59.73333333 19.22844445 85.78844445L229.03466667 695.18222222c-49.72088889-45.73866667-92.72888889-106.60977778-129.82044445-183.06844444C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c58.368 0 111.84355555 9.44355555 160.65422222 28.55822222l-79.41688889 79.41688888z" p-id="908" fill="#CED0D6"></path><path d="M507.44888889 639.43111111c-7.28177778 0-14.44977778-0.56888889-21.39022222-1.82044444l-58.14044445 58.14044444c24.34844445 10.58133333 51.31377778 16.384 79.53066667 16.384 110.592 0 200.24888889-89.65688889 200.24888889-200.24888889 0-28.21688889-5.80266667-55.18222222-16.384-79.53066667l-58.14044445 58.14044445c1.13777778 6.94044445 1.82044445 14.10844445 1.82044445 21.39022222C634.88 582.42844445 577.87733333 639.43111111 507.44888889 639.43111111z" p-id="909" fill="#CED0D6"></path></svg>
width="16" height="16">
<path d="M1001.472 482.64533333C893.61066667 255.43111111 730.56711111 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556C130.38933333 768.56888889 293.43288889 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0-58.59555556zM512 800.99555555c-183.52355555 0-317.89511111-93.07022222-412.672-288.99555555C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c183.52355555 0 317.89511111 93.07022222 412.672 288.99555555C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555z"
p-id="878" fill="#CED0D6"></path>
<path d="M507.73333333 324.26666667c-103.68 0-187.73333333 84.05333333-187.73333333 187.73333333s84.05333333 187.73333333 187.73333333 187.73333333 187.73333333-84.05333333 187.73333334-187.73333333-84.05333333-187.73333333-187.73333334-187.73333333z m0 307.2c-66.02666667 0-119.46666667-53.44-119.46666666-119.46666667s53.44-119.46666667 119.46666666-119.46666667 119.46666667 53.44 119.46666667 119.46666667-53.44 119.46666667-119.46666667 119.46666667z"
p-id="879" fill="#CED0D6"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB