mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: SFTP supports pasting files for upload (#87)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
}
|
||||
copyLocalFileToFileSystem(files.filterIsInstance<File>())
|
||||
}
|
||||
}.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<File>())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Disposer.register(this, object : Disposable {
|
||||
override fun dispose() {
|
||||
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)
|
||||
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"))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
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>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user