mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support transfer virtual window
This commit is contained in:
@@ -20,6 +20,7 @@ object Icons {
|
|||||||
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||||
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||||
|
val questionMark by lazy { DynamicIcon("icons/questionMark.svg", "icons/questionMark_dark.svg") }
|
||||||
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
||||||
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
||||||
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
|
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
|
||||||
@@ -119,6 +120,7 @@ object Icons {
|
|||||||
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
||||||
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
|
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
|
||||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||||
|
val transferToolWindow by lazy { DynamicIcon("icons/transferToolWindow.svg", "icons/transferToolWindow_dark.svg") }
|
||||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||||
val applyNotConflictsLeft by lazy {
|
val applyNotConflictsLeft by lazy {
|
||||||
DynamicIcon(
|
DynamicIcon(
|
||||||
|
|||||||
@@ -146,6 +146,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop
|
||||||
|
stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import app.termora.snippet.SnippetTreeDialog
|
|||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||||
|
import app.termora.terminal.panel.vw.TransferVisualWindow
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -118,6 +119,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
// 服务器信息
|
// 服务器信息
|
||||||
add(initServerInfoActionButton())
|
add(initServerInfoActionButton())
|
||||||
|
|
||||||
|
// Transfer
|
||||||
|
add(initTransferActionButton())
|
||||||
|
|
||||||
// Snippet
|
// Snippet
|
||||||
add(initSnippetActionButton())
|
add(initSnippetActionButton())
|
||||||
|
|
||||||
@@ -185,6 +189,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initTransferActionButton(): JButton {
|
||||||
|
val btn = JButton(Icons.folder)
|
||||||
|
btn.toolTipText = I18n.getString("termora.transport.sftp")
|
||||||
|
btn.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
|
if (tab !is SSHTerminalTab) {
|
||||||
|
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (window in terminalPanel.getVisualWindows()) {
|
||||||
|
if (window is TransferVisualWindow) {
|
||||||
|
terminalPanel.moveToFront(window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val visualWindowPanel = TransferVisualWindow(tab, terminalPanel)
|
||||||
|
terminalPanel.addVisualWindow(visualWindowPanel)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
private fun initSnippetActionButton(): JButton {
|
private fun initSnippetActionButton(): JButton {
|
||||||
val btn = JButton(Icons.codeSpan)
|
val btn = JButton(Icons.codeSpan)
|
||||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||||
|
|||||||
@@ -588,6 +588,7 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
requestFocusInWindow()
|
requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
||||||
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
||||||
for (name in windows.split(",")) {
|
for (name in windows.split(",")) {
|
||||||
@@ -605,6 +606,13 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
this
|
this
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else if (name == "Transfer") {
|
||||||
|
addVisualWindow(
|
||||||
|
TransferVisualWindow(
|
||||||
|
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
|
|||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toolbarButtons(): List<JButton> {
|
override fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||||
return listOf(percentageBtn)
|
return listOf(percentageBtn to Position.Right)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||||
|
import app.termora.plugin.internal.ssh.SSHTerminalTab.Companion.SSHSession
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.DataListener
|
||||||
|
import app.termora.transfer.*
|
||||||
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
|
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.exception.ExceptionUtils
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||||
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.reflect.cast
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
|
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(TransferVisualWindow::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class State {
|
||||||
|
Connecting,
|
||||||
|
Transfer,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val executorService = Executors.newVirtualThreadPerTaskExecutor()
|
||||||
|
private val coroutineDispatcher = executorService.asCoroutineDispatcher()
|
||||||
|
private val coroutineScope = CoroutineScope(coroutineDispatcher)
|
||||||
|
private val cardLayout = CardLayout()
|
||||||
|
private val panel = JPanel(cardLayout)
|
||||||
|
private val connectingPanel = ConnectingPanel()
|
||||||
|
private val connectFailedPanel = ConnectFailedPanel()
|
||||||
|
private val transferManager = TransferTableModel(coroutineScope)
|
||||||
|
private val disposable = Disposer.newDisposable()
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val questionBtn = JButton(Icons.questionMark)
|
||||||
|
private val badgeIcon = BadgeIcon(Icons.download)
|
||||||
|
private val downloadBtn = JButton(badgeIcon)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initViews()
|
||||||
|
initEvents()
|
||||||
|
initVisualWindowPanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initViews() {
|
||||||
|
title = "SFTP"
|
||||||
|
|
||||||
|
panel.add(connectingPanel, State.Connecting.name)
|
||||||
|
panel.add(connectFailedPanel, State.Failed.name)
|
||||||
|
|
||||||
|
|
||||||
|
add(panel, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(tab, this)
|
||||||
|
Disposer.register(this, disposable)
|
||||||
|
|
||||||
|
connectingPanel.busyLabel.isBusy = true
|
||||||
|
|
||||||
|
val terminal = tab.getData(DataProviders.TerminalPanel)?.getData(DataProviders.Terminal)
|
||||||
|
terminal?.getTerminalModel()?.addDataListener(object : DataListener {
|
||||||
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
// https://github.com/TermoraDev/termora/pull/244
|
||||||
|
if (key == DataKey.CurrentDir) {
|
||||||
|
val dir = DataKey.CurrentDir.clazz.cast(data)
|
||||||
|
val navigator = getTransportNavigator() ?: return
|
||||||
|
val path = navigator.getFileSystem().getPath(dir)
|
||||||
|
if (path == navigator.workdir) return
|
||||||
|
navigator.navigateTo(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
downloadBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = DownloadDialog()
|
||||||
|
dialog.setLocationRelativeTo(downloadBtn)
|
||||||
|
dialog.setLocation(dialog.x, downloadBtn.locationOnScreen.y + downloadBtn.height + 1)
|
||||||
|
dialog.isVisible = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
transferManager.addTransferListener(object : TransferListener {
|
||||||
|
override fun onTransferCountChanged() {
|
||||||
|
val oldVisible = badgeIcon.visible
|
||||||
|
val newVisible = transferManager.getTransferCount() > 0
|
||||||
|
if (oldVisible != newVisible) {
|
||||||
|
badgeIcon.visible = newVisible
|
||||||
|
downloadBtn.repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 立即连接
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connect() {
|
||||||
|
connectingPanel.busyLabel.isBusy = true
|
||||||
|
cardLayout.show(panel, State.Connecting.name)
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
|
||||||
|
try {
|
||||||
|
val session = getSession()
|
||||||
|
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
|
val support = TransportSupport(fileSystem, fileSystem.defaultDir.absolutePathString())
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
val internalTransferManager = MyInternalTransferManager()
|
||||||
|
val transportPanel = TransportPanel(
|
||||||
|
internalTransferManager, tab.host,
|
||||||
|
TransportSupportLoader { support })
|
||||||
|
internalTransferManager.setTransferPanel(transportPanel)
|
||||||
|
|
||||||
|
Disposer.register(transportPanel, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
panel.remove(transportPanel)
|
||||||
|
IOUtils.closeQuietly(fileSystem)
|
||||||
|
swingCoroutineScope.launch {
|
||||||
|
connectFailedPanel.errorLabel.text = I18n.getString("termora.transport.sftp.closed")
|
||||||
|
cardLayout.show(panel, State.Failed.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Disposer.register(disposable, transportPanel)
|
||||||
|
|
||||||
|
// 如果 session 关闭,立即销毁 Transfer
|
||||||
|
session.addCloseFutureListener { Disposer.dispose(transportPanel) }
|
||||||
|
panel.add(transportPanel, State.Transfer.name)
|
||||||
|
cardLayout.show(panel, State.Transfer.name)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
connectFailedPanel.errorLabel.text = ExceptionUtils.getRootCauseMessage(e)
|
||||||
|
cardLayout.show(panel, State.Failed.name)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
swingCoroutineScope.launch { connectingPanel.busyLabel.isBusy = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSession(): ClientSession {
|
||||||
|
while (coroutineScope.isActive) {
|
||||||
|
val session = tab.getData(SSHSession)
|
||||||
|
if (session == null) {
|
||||||
|
delay(250.milliseconds)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Session is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTransportNavigator(): TransportPanel? {
|
||||||
|
for (i in 0 until panel.componentCount) {
|
||||||
|
val c = panel.getComponent(i)
|
||||||
|
if (c is TransportPanel) {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeClose(): Boolean {
|
||||||
|
if (transferManager.getTransferCount() > 0) {
|
||||||
|
return OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.transport.sftp.close-tab"),
|
||||||
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
}
|
||||||
|
return super.beforeClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
coroutineDispatcher.close()
|
||||||
|
executorService.shutdownNow()
|
||||||
|
connectingPanel.busyLabel.isBusy = false
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||||
|
return listOf(downloadBtn to Position.Left, questionBtn to Position.Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DownloadDialog() : JDialog() {
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||||
|
isModal = false
|
||||||
|
title = I18n.getString("termora.transport.sftp")
|
||||||
|
isUndecorated = true
|
||||||
|
layout = BorderLayout()
|
||||||
|
add(createCenterPanel(), BorderLayout.CENTER)
|
||||||
|
val window = this
|
||||||
|
|
||||||
|
addWindowFocusListener(object : WindowAdapter() {
|
||||||
|
override fun windowLostFocus(e: WindowEvent?) {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val inputMap = rootPane.getInputMap(WHEN_IN_FOCUSED_WINDOW)
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close")
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
|
||||||
|
rootPane.actionMap.put("close", object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (hasPopupMenus()) return
|
||||||
|
SwingUtilities.invokeLater { window.dispose() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPopupMenus(): Boolean {
|
||||||
|
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
if (c != null) {
|
||||||
|
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||||
|
JPopupMenu::class.java,
|
||||||
|
c as Container, true
|
||||||
|
)
|
||||||
|
|
||||||
|
var openPopup = false
|
||||||
|
for (p in popups) {
|
||||||
|
p.isVisible = false
|
||||||
|
openPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val window = c as? Window ?: SwingUtilities.windowForComponent(c)
|
||||||
|
if (window != null) {
|
||||||
|
val windows = window.ownedWindows
|
||||||
|
for (w in windows) {
|
||||||
|
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
||||||
|
openPopup = true
|
||||||
|
w.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openPopup) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCenterPanel(): JComponent {
|
||||||
|
val table = TransferTable(coroutineScope, transferManager)
|
||||||
|
val scrollPane = JScrollPane(table)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
removeWindowListener(this)
|
||||||
|
Disposer.dispose(table)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return scrollPane
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyInternalTransferManager() : InternalTransferManager {
|
||||||
|
private lateinit var internalTransferManager: InternalTransferManager
|
||||||
|
|
||||||
|
fun setTransferPanel(transportPanel: TransportPanel) {
|
||||||
|
internalTransferManager = createInternalTransferManager(transportPanel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canTransfer(paths: List<Path>): Boolean {
|
||||||
|
return paths.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransfer(
|
||||||
|
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||||
|
mode: InternalTransferManager.TransferMode
|
||||||
|
): CompletableFuture<Unit> {
|
||||||
|
|
||||||
|
if (mode == InternalTransferManager.TransferMode.Transfer) {
|
||||||
|
val future = CompletableFuture<Unit>()
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.showOpenDialog(owner).whenComplete { files, e ->
|
||||||
|
if (e != null) {
|
||||||
|
future.completeExceptionally(e)
|
||||||
|
} else if (files.isEmpty()) {
|
||||||
|
future.complete(Unit)
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
try {
|
||||||
|
addTransfer(paths, files.first().toPath(), mode)
|
||||||
|
.thenApply { future.complete(it) }
|
||||||
|
.exceptionally { future.completeExceptionally(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
future.completeExceptionally(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalTransferManager.addTransfer(paths, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransfer(
|
||||||
|
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||||
|
targetWorkdir: Path,
|
||||||
|
mode: InternalTransferManager.TransferMode
|
||||||
|
): CompletableFuture<Unit> {
|
||||||
|
return internalTransferManager.addTransfer(paths, targetWorkdir, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addHighTransfer(source: Path, target: Path): String {
|
||||||
|
return internalTransferManager.addHighTransfer(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransferListener(listener: TransferListener): Disposable {
|
||||||
|
return transferManager.addTransferListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createWorkdirProvider(transportPanel: TransportPanel): DefaultInternalTransferManager.WorkdirProvider {
|
||||||
|
return object : DefaultInternalTransferManager.WorkdirProvider {
|
||||||
|
override fun getWorkdir(): Path? {
|
||||||
|
return transportPanel.workdir
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTableModel(): TransportTableModel? {
|
||||||
|
return transportPanel.getTableModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInternalTransferManager(transportPanel: TransportPanel): InternalTransferManager {
|
||||||
|
return DefaultInternalTransferManager(
|
||||||
|
{ owner },
|
||||||
|
coroutineScope,
|
||||||
|
transferManager,
|
||||||
|
object : DefaultInternalTransferManager.WorkdirProvider {
|
||||||
|
override fun getWorkdir() = null
|
||||||
|
override fun getTableModel() = null
|
||||||
|
},
|
||||||
|
createWorkdirProvider(transportPanel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class ConnectingPanel : JPanel(BorderLayout()) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
add(builder.build(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ import kotlin.math.min
|
|||||||
open class VisualWindowPanel(protected val id: String, protected val visualWindowManager: VisualWindowManager) :
|
open class VisualWindowPanel(protected val id: String, protected val visualWindowManager: VisualWindowManager) :
|
||||||
JPanel(BorderLayout()), VisualWindow {
|
JPanel(BorderLayout()), VisualWindow {
|
||||||
|
|
||||||
|
protected enum class Position {
|
||||||
|
Left, Right
|
||||||
|
}
|
||||||
|
|
||||||
private val stickPx = 2
|
private val stickPx = 2
|
||||||
protected val properties get() = DatabaseManager.getInstance().properties
|
protected val properties get() = DatabaseManager.getInstance().properties
|
||||||
private val titleLabel = JLabel()
|
private val titleLabel = JLabel()
|
||||||
@@ -90,7 +94,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
alwaysTopBtn.isVisible = false
|
alwaysTopBtn.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun toolbarButtons(): List<JButton> {
|
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,19 +168,19 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initToolBar() {
|
private fun initToolBar() {
|
||||||
val btns = toolbarButtons()
|
val buttons = toolbarButtons()
|
||||||
val count = 2 + btns.size
|
val count = 2 + buttons.size - (buttons.count { it.second == Position.Left } * 2)
|
||||||
toolbar.add(alwaysTopBtn)
|
toolbar.add(alwaysTopBtn)
|
||||||
toolbar.add(Box.createHorizontalStrut(count * 26))
|
buttons.filter { it.second == Position.Left }.forEach { toolbar.add(it.first) }
|
||||||
toolbar.add(JLabel(Icons.empty))
|
toolbar.add(Box.createHorizontalStrut(max(count * 26, 0)))
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
toolbar.add(titleLabel)
|
toolbar.add(titleLabel)
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
btns.forEach { toolbar.add(it) }
|
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
|
||||||
|
|
||||||
toolbar.add(toggleWindowBtn)
|
toolbar.add(toggleWindowBtn)
|
||||||
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
|
toolbar.add(JButton(Icons.close).apply { addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) } })
|
||||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||||
add(toolbar, BorderLayout.NORTH)
|
add(toolbar, BorderLayout.NORTH)
|
||||||
}
|
}
|
||||||
@@ -318,6 +322,9 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(this) }
|
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun beforeClose(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun close() {
|
protected open fun close() {
|
||||||
if (isWindow()) {
|
if (isWindow()) {
|
||||||
|
|||||||
39
src/main/kotlin/app/termora/transfer/BadgeIcon.kt
Normal file
39
src/main/kotlin/app/termora/transfer/BadgeIcon.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import app.termora.restore
|
||||||
|
import app.termora.save
|
||||||
|
import app.termora.setupAntialiasing
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Graphics
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeIcon(
|
||||||
|
private val icon: Icon,
|
||||||
|
var visible: Boolean = false
|
||||||
|
) : Icon {
|
||||||
|
|
||||||
|
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
|
||||||
|
icon.paintIcon(c, g, x, y)
|
||||||
|
if (g is Graphics2D) {
|
||||||
|
if (visible) {
|
||||||
|
g.save()
|
||||||
|
setupAntialiasing(g)
|
||||||
|
val size = 6
|
||||||
|
g.color = UIManager.getColor("Component.error.focusedBorderColor")
|
||||||
|
g.fillRoundRect(c.width - size - 4, 4, size, size, size, size)
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconWidth(): Int {
|
||||||
|
return icon.iconWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconHeight(): Int {
|
||||||
|
return icon.iconHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.transfer.InternalTransferManager.TransferMode
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.FileVisitor
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.function.Supplier
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.collections.ArrayDeque
|
||||||
|
import kotlin.collections.List
|
||||||
|
import kotlin.collections.Set
|
||||||
|
import kotlin.collections.isNotEmpty
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class DefaultInternalTransferManager(
|
||||||
|
private val owner: Supplier<Window>,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val transferManager: TransferManager,
|
||||||
|
private val source: WorkdirProvider,
|
||||||
|
private val target: WorkdirProvider
|
||||||
|
) : InternalTransferManager {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(DefaultInternalTransferManager::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkdirProvider {
|
||||||
|
fun getWorkdir(): Path?
|
||||||
|
fun getTableModel(): TransportTableModel?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private data class AskTransfer(
|
||||||
|
val option: Int,
|
||||||
|
val action: TransferAction,
|
||||||
|
val applyAll: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class AskTransferContext(var action: TransferAction, var applyAll: Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
override fun canTransfer(paths: List<Path>): Boolean {
|
||||||
|
return paths.isNotEmpty() && target.getWorkdir() != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransfer(
|
||||||
|
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||||
|
mode: TransferMode
|
||||||
|
): CompletableFuture<Unit> {
|
||||||
|
val workdir = (if (mode == TransferMode.Delete || mode == TransferMode.ChangePermission)
|
||||||
|
source.getWorkdir() ?: target.getWorkdir() else target.getWorkdir()) ?: throw IllegalStateException()
|
||||||
|
return addTransfer(paths, workdir, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransfer(
|
||||||
|
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||||
|
targetWorkdir: Path,
|
||||||
|
mode: TransferMode
|
||||||
|
): CompletableFuture<Unit> {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
if (paths.isEmpty()) return CompletableFuture.completedFuture(Unit)
|
||||||
|
|
||||||
|
val future = CompletableFuture<Unit>()
|
||||||
|
val tableModel = when (targetWorkdir.fileSystem) {
|
||||||
|
source.getWorkdir()?.fileSystem -> source.getTableModel()
|
||||||
|
target.getWorkdir()?.fileSystem -> target.getTableModel()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val context = AskTransferContext(TransferAction.Overwrite, false)
|
||||||
|
for (pair in paths) {
|
||||||
|
if (mode == TransferMode.Transfer && tableModel != null && context.applyAll.not()) {
|
||||||
|
val action = withContext(Dispatchers.Swing) {
|
||||||
|
getTransferAction(context, tableModel, pair.second)
|
||||||
|
}
|
||||||
|
if (action == null) {
|
||||||
|
break
|
||||||
|
} else if (context.applyAll) {
|
||||||
|
if (action == TransferAction.Skip) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (action == TransferAction.Skip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val flag = doAddTransfer(targetWorkdir, pair, mode, context.action, future)
|
||||||
|
if (flag != FileVisitResult.CONTINUE) break
|
||||||
|
}
|
||||||
|
future.complete(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) log.error(e.message, e)
|
||||||
|
future.completeExceptionally(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addHighTransfer(source: Path, target: Path): String {
|
||||||
|
val transfer = FileTransfer(
|
||||||
|
parentId = StringUtils.EMPTY,
|
||||||
|
source = source,
|
||||||
|
target = target,
|
||||||
|
size = Files.size(source),
|
||||||
|
action = TransferAction.Overwrite,
|
||||||
|
priority = Transfer.Priority.High
|
||||||
|
)
|
||||||
|
if (transferManager.addTransfer(transfer)) {
|
||||||
|
return transfer.id()
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("Cannot add high transfer.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTransferListener(listener: TransferListener): Disposable {
|
||||||
|
return transferManager.addTransferListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTransferAction(
|
||||||
|
context: AskTransferContext,
|
||||||
|
model: TransportTableModel,
|
||||||
|
source: TransportTableModel.Attributes
|
||||||
|
): TransferAction? {
|
||||||
|
if (context.applyAll) return context.action
|
||||||
|
|
||||||
|
for (i in 0 until model.rowCount) {
|
||||||
|
val c = model.getAttributes(i)
|
||||||
|
if (c.name != source.name) continue
|
||||||
|
val transfer = askTransfer(source, c)
|
||||||
|
context.action = transfer.action
|
||||||
|
context.applyAll = transfer.applyAll
|
||||||
|
if (transfer.option != JOptionPane.OK_OPTION) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.action
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun askTransfer(
|
||||||
|
source: TransportTableModel.Attributes,
|
||||||
|
target: TransportTableModel.Attributes
|
||||||
|
): AskTransfer {
|
||||||
|
val formMargin = "7dlu"
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, 2dlu, left:pref",
|
||||||
|
"pref, 12dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val iconSize = 36
|
||||||
|
// @formatter:off
|
||||||
|
val targetIcon = ScaleIcon(if(target.isDirectory) NativeIcons.folderIcon else NativeIcons.fileIcon, iconSize)
|
||||||
|
val sourceIcon = ScaleIcon(if(source.isDirectory) NativeIcons.folderIcon else NativeIcons.fileIcon, iconSize)
|
||||||
|
val sourceModified= DateFormatUtils.format(Date(source.lastModifiedTime), I18n.getString("termora.date-format"))
|
||||||
|
val targetModified= DateFormatUtils.format(Date(target.lastModifiedTime), I18n.getString("termora.date-format"))
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
|
||||||
|
val actionsComBoBox = JComboBox<TransferAction>()
|
||||||
|
actionsComBoBox.addItem(TransferAction.Overwrite)
|
||||||
|
actionsComBoBox.addItem(TransferAction.Append)
|
||||||
|
actionsComBoBox.addItem(TransferAction.Skip)
|
||||||
|
actionsComBoBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: StringUtils.EMPTY
|
||||||
|
if (value == TransferAction.Overwrite) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
||||||
|
} else if (value == TransferAction.Skip) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
||||||
|
} else if (value == TransferAction.Append) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.append")
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val applyAllCheckbox = JCheckBox(I18n.getString("termora.transport.sftp.already-exists.apply-all"))
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(actionsComBoBox)
|
||||||
|
box.add(Box.createHorizontalStrut(8))
|
||||||
|
box.add(applyAllCheckbox)
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
val ttBox = Box.createVerticalBox()
|
||||||
|
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message1")))
|
||||||
|
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message2")))
|
||||||
|
|
||||||
|
val warningIcon = ScaleIcon(Icons.warningIntroduction, iconSize)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
// tip
|
||||||
|
.add(JLabel(warningIcon)).xy(1, rows)
|
||||||
|
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// name
|
||||||
|
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
||||||
|
.add(source.name).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// separator
|
||||||
|
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||||
|
// Destination
|
||||||
|
.add("${I18n.getString("termora.transport.sftp.already-exists.destination")}:").xy(1, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
// Folder
|
||||||
|
.add(JLabel(targetIcon)).xy(1, rows, "center, fill")
|
||||||
|
.add(targetModified).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// Source
|
||||||
|
.add("${I18n.getString("termora.transport.sftp.already-exists.source")}:").xy(1, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
// Folder
|
||||||
|
.add(JLabel(sourceIcon)).xy(1, rows, "center, fill")
|
||||||
|
.add(sourceModified).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// separator
|
||||||
|
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||||
|
// name
|
||||||
|
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.actions")}:")).xy(1, rows)
|
||||||
|
.add(box).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
panel.putClientProperty("SKIP_requestFocusInWindow", true)
|
||||||
|
|
||||||
|
return AskTransfer(
|
||||||
|
option = OptionPane.showConfirmDialog(
|
||||||
|
owner.get(), panel,
|
||||||
|
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
title = source.name,
|
||||||
|
initialValue = JOptionPane.OK_OPTION,
|
||||||
|
) {
|
||||||
|
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
||||||
|
it.setLocationRelativeTo(it.owner)
|
||||||
|
},
|
||||||
|
action = actionsComBoBox.selectedItem as TransferAction,
|
||||||
|
applyAll = applyAllCheckbox.isSelected
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doAddTransfer(
|
||||||
|
workdir: Path,
|
||||||
|
pair: Pair<Path, TransportTableModel.Attributes>,
|
||||||
|
mode: TransferMode,
|
||||||
|
action: TransferAction,
|
||||||
|
future: CompletableFuture<Unit>
|
||||||
|
): FileVisitResult {
|
||||||
|
|
||||||
|
val isDirectory = pair.second.isDirectory
|
||||||
|
val path = pair.first
|
||||||
|
if (isDirectory.not()) {
|
||||||
|
val transfer = createTransfer(path, workdir.resolve(path.name), false, StringUtils.EMPTY, mode, action)
|
||||||
|
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
val continued = AtomicBoolean(true)
|
||||||
|
val queue = ArrayDeque<Transfer>()
|
||||||
|
val isCancelled =
|
||||||
|
{ (future.isCancelled || future.isCompletedExceptionally).apply { continued.set(this.not()) } }
|
||||||
|
val basedir = if (isDirectory) workdir.resolve(path.name) else workdir
|
||||||
|
val visitor = object : FileVisitor<Path> {
|
||||||
|
override fun preVisitDirectory(
|
||||||
|
dir: Path,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
val parentId = queue.lastOrNull()?.id() ?: StringUtils.EMPTY
|
||||||
|
// @formatter:off
|
||||||
|
val transfer = when (mode) {
|
||||||
|
TransferMode.Delete -> createTransfer(dir, dir, true, parentId, mode, action)
|
||||||
|
TransferMode.ChangePermission -> createTransfer(path, dir, true, parentId, mode, action, pair.second.permissions)
|
||||||
|
else -> createTransfer(dir, basedir.resolve(path.relativize(dir).pathString), true, parentId, mode, action)
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
queue.addLast(transfer)
|
||||||
|
|
||||||
|
if (transferManager.addTransfer(transfer).not()) {
|
||||||
|
continued.set(false)
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFile(
|
||||||
|
file: Path,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
|
||||||
|
val parentId = queue.last().id()
|
||||||
|
// @formatter:off
|
||||||
|
val transfer = when (mode) {
|
||||||
|
TransferMode.Delete -> createTransfer(file, file, false, parentId, mode, action)
|
||||||
|
TransferMode.ChangePermission -> createTransfer(file, file, false, parentId, mode, action, pair.second.permissions)
|
||||||
|
else -> createTransfer(file, basedir.resolve(path.relativize(file).pathString), false, parentId, mode, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferManager.addTransfer(transfer).not()) {
|
||||||
|
continued.set(false)
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFileFailed(
|
||||||
|
file: Path?,
|
||||||
|
exc: IOException
|
||||||
|
): FileVisitResult {
|
||||||
|
if (log.isErrorEnabled) log.error(exc.message, exc)
|
||||||
|
future.completeExceptionally(exc)
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postVisitDirectory(
|
||||||
|
dir: Path?,
|
||||||
|
exc: IOException?
|
||||||
|
): FileVisitResult {
|
||||||
|
val c = queue.removeLast()
|
||||||
|
if (c is TransferScanner) c.scanned()
|
||||||
|
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PathWalker.walkFileTree(path, visitor)
|
||||||
|
|
||||||
|
// 已经添加的则继续传输
|
||||||
|
while (queue.isNotEmpty()) {
|
||||||
|
val c = queue.removeLast()
|
||||||
|
if (c is TransferScanner) c.scanned()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (continued.get()) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createTransfer(
|
||||||
|
source: Path,
|
||||||
|
target: Path,
|
||||||
|
isDirectory: Boolean,
|
||||||
|
parentId: String,
|
||||||
|
mode: TransferMode,
|
||||||
|
action:TransferAction,
|
||||||
|
permissions: Set<PosixFilePermission>? = null
|
||||||
|
): Transfer {
|
||||||
|
if (mode == TransferMode.Delete) {
|
||||||
|
return DeleteTransfer(
|
||||||
|
parentId,
|
||||||
|
source,
|
||||||
|
isDirectory,
|
||||||
|
if (isDirectory) 1 else Files.size(source)
|
||||||
|
)
|
||||||
|
} else if (mode == TransferMode.ChangePermission) {
|
||||||
|
if (permissions == null) throw IllegalStateException()
|
||||||
|
return ChangePermissionTransfer(
|
||||||
|
parentId,
|
||||||
|
target,
|
||||||
|
isDirectory = isDirectory,
|
||||||
|
permissions = permissions,
|
||||||
|
size = if (isDirectory) 1 else Files.size(target)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
return DirectoryTransfer(
|
||||||
|
parentId = parentId,
|
||||||
|
source = source,
|
||||||
|
target = target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileTransfer(
|
||||||
|
parentId = parentId,
|
||||||
|
source = source,
|
||||||
|
target = target,
|
||||||
|
action = action,
|
||||||
|
size = Files.size(source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,5 +6,7 @@ interface TransferListener : EventListener {
|
|||||||
/**
|
/**
|
||||||
* 状态变化
|
* 状态变化
|
||||||
*/
|
*/
|
||||||
fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State)
|
fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {}
|
||||||
|
|
||||||
|
fun onTransferCountChanged() {}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ import java.awt.Insets
|
|||||||
import java.awt.event.ActionEvent
|
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.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.tree.DefaultTreeCellRenderer
|
import javax.swing.tree.DefaultTreeCellRenderer
|
||||||
@@ -30,7 +31,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
|||||||
|
|
||||||
private val table get() = this
|
private val table get() = this
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val disposed = AtomicBoolean(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -153,7 +154,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
|||||||
private fun refreshView() {
|
private fun refreshView() {
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
val timeout = 500
|
val timeout = 500
|
||||||
while (coroutineScope.isActive) {
|
while (coroutineScope.isActive && disposed.get().not()) {
|
||||||
for (row in 0 until rowCount) {
|
for (row in 0 until rowCount) {
|
||||||
val treePath = getPathForRow(row) ?: continue
|
val treePath = getPathForRow(row) ?: continue
|
||||||
val node = treePath.lastPathComponent as? DefaultMutableTreeTableNode ?: continue
|
val node = treePath.lastPathComponent as? DefaultMutableTreeTableNode ?: continue
|
||||||
@@ -164,6 +165,10 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
disposed.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer() {
|
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer() {
|
||||||
private var progress = 0.0
|
private var progress = 0.0
|
||||||
private var progressInt = 0
|
private var progressInt = 0
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
|||||||
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import java.io.InterruptedIOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -114,7 +115,7 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
val result = AtomicBoolean(false)
|
val result = AtomicBoolean(false)
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
if (validGrandfather(node.transfer.parentId())) {
|
if (validGrandfather(node.transfer.parentId())) {
|
||||||
map[node.transfer.id()] = node
|
putNodeToMap(node.transfer.id(), node)
|
||||||
insertNodeInto(node, parent, parent.childCount)
|
insertNodeInto(node, parent, parent.childCount)
|
||||||
result.set(true)
|
result.set(true)
|
||||||
}
|
}
|
||||||
@@ -124,6 +125,21 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
return result.get()
|
return result.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeNodeFromMap(id: String) {
|
||||||
|
if (map.remove(id) != null) {
|
||||||
|
for (listener in eventListener.getListeners(TransferListener::class.java)) {
|
||||||
|
listener.onTransferCountChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putNodeToMap(id: String, node: TransferTreeTableNode) {
|
||||||
|
map[id] = node
|
||||||
|
for (listener in eventListener.getListeners(TransferListener::class.java)) {
|
||||||
|
listener.onTransferCountChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取祖先的状态,如果祖先状态不正常,那么子直接定义为失败
|
* 获取祖先的状态,如果祖先状态不正常,那么子直接定义为失败
|
||||||
*
|
*
|
||||||
@@ -179,7 +195,7 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
// 定义为失败
|
// 定义为失败
|
||||||
node.tryChangeState(State.Failed)
|
node.tryChangeState(State.Failed)
|
||||||
// 移除
|
// 移除
|
||||||
map.remove(node.transfer.id())
|
removeNodeFromMap(node.transfer.id())
|
||||||
removeNodeFromParent(node)
|
removeNodeFromParent(node)
|
||||||
|
|
||||||
// 如果删除时还在传输,那么需要减去大小
|
// 如果删除时还在传输,那么需要减去大小
|
||||||
@@ -388,6 +404,10 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
lock.withLock { condition.signalAll() }
|
lock.withLock { condition.signalAll() }
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
break
|
break
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
break
|
||||||
|
} catch (_: InterruptedIOException) {
|
||||||
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) log.error(e.message, e)
|
if (log.isErrorEnabled) log.error(e.message, e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.formdev.flatlaf.icons.FlatTreeLeafIcon
|
|||||||
import com.formdev.flatlaf.util.SystemInfo
|
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.IOUtils
|
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -46,7 +45,6 @@ import java.nio.file.attribute.PosixFilePermissions
|
|||||||
import java.text.MessageFormat
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -120,7 +118,7 @@ class TransportPanel(
|
|||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
private val disposed = AtomicBoolean(false)
|
private val disposed = AtomicBoolean(false)
|
||||||
private val futures = CopyOnWriteArraySet<Future<*>>()
|
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
||||||
|
|
||||||
private val _fileSystem by lazy { getSupport().fileSystem }
|
private val _fileSystem by lazy { getSupport().fileSystem }
|
||||||
private val defaultPath by lazy { getSupport().path }
|
private val defaultPath by lazy { getSupport().path }
|
||||||
@@ -456,6 +454,9 @@ class TransportPanel(
|
|||||||
|
|
||||||
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||||
showContextmenu(rows, e)
|
showContextmenu(rows, e)
|
||||||
|
} else if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
|
val row = table.rowAtPoint(e.point)
|
||||||
|
if (row < 0) table.clearSelection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -627,7 +628,7 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reload(oldPath: Path? = workdir, newPath: Path? = workdir, requestFocus: Boolean = true): Boolean {
|
private fun reload(oldPath: Path? = workdir, newPath: Path? = workdir, requestFocus: Boolean = false): Boolean {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
if (loading) return false
|
if (loading) return false
|
||||||
@@ -663,7 +664,7 @@ class TransportPanel(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doReload(oldPath: Path? = null, newPath: Path? = null, requestFocus: Boolean = true): Path {
|
private suspend fun doReload(oldPath: Path? = null, newPath: Path? = null, requestFocus: Boolean = false): Path {
|
||||||
|
|
||||||
val workdir = newPath ?: oldPath
|
val workdir = newPath ?: oldPath
|
||||||
|
|
||||||
@@ -854,7 +855,6 @@ class TransportPanel(
|
|||||||
futures.forEach { it.cancel(true) }
|
futures.forEach { it.cancel(true) }
|
||||||
futures.clear()
|
futures.clear()
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
if (loader.isLoaded && _fileSystem.isOpen) IOUtils.closeQuietly(_fileSystem)
|
|
||||||
loadingPanel.busyLabel.isBusy = false
|
loadingPanel.busyLabel.isBusy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1038,10 +1038,12 @@ class TransportPanel(
|
|||||||
val path = files.first().first
|
val path = files.first().first
|
||||||
processPath(path.name) {
|
processPath(path.name) {
|
||||||
if (c.includeSubFolder) {
|
if (c.includeSubFolder) {
|
||||||
val future = transferManager.addTransfer(
|
val future = withContext(Dispatchers.Swing) {
|
||||||
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
transferManager.addTransfer(
|
||||||
InternalTransferManager.TransferMode.ChangePermission
|
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
||||||
)
|
InternalTransferManager.TransferMode.ChangePermission
|
||||||
|
)
|
||||||
|
}
|
||||||
mountFuture(future)
|
mountFuture(future)
|
||||||
future.get()
|
future.get()
|
||||||
} else {
|
} else {
|
||||||
@@ -1064,7 +1066,7 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processPath(name: String, action: () -> Unit) {
|
private fun processPath(name: String, action: suspend () -> Unit) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
action.invoke()
|
action.invoke()
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import javax.swing.SwingUtilities
|
|||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class TransportTabbed(
|
class TransportTabbed(
|
||||||
private val transferManager: TransferManager,
|
private val transferManager: TransferManager,
|
||||||
private val internalTransferManager: InternalTransferManager
|
|
||||||
) : FlatTabbedPane(), Disposable {
|
) : FlatTabbedPane(), Disposable {
|
||||||
private val addBtn = JButton(Icons.add)
|
private val addBtn = JButton(Icons.add)
|
||||||
private val tabbed get() = this
|
private val tabbed get() = this
|
||||||
|
lateinit var internalTransferManager: InternalTransferManager
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initViews()
|
initViews()
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.DynamicColor
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.transfer.InternalTransferManager.TransferMode
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import kotlinx.coroutines.Dispatchers
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.io.IOException
|
import java.nio.file.Path
|
||||||
import java.nio.file.*
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.collections.ArrayDeque
|
|
||||||
import kotlin.collections.List
|
|
||||||
import kotlin.collections.Set
|
|
||||||
import kotlin.collections.isNotEmpty
|
|
||||||
import kotlin.io.path.name
|
|
||||||
import kotlin.io.path.pathString
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
|
|
||||||
class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
@@ -41,10 +24,10 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
private val splitPane = JSplitPane()
|
private val splitPane = JSplitPane()
|
||||||
private val transferManager = TransferTableModel(coroutineScope)
|
private val transferManager = TransferTableModel(coroutineScope)
|
||||||
private val transferTable = TransferTable(coroutineScope, transferManager)
|
private val transferTable = TransferTable(coroutineScope, transferManager)
|
||||||
private val leftTransferManager = MyInternalTransferManager()
|
private val leftTabbed = TransportTabbed(transferManager)
|
||||||
private val rightTransferManager = MyInternalTransferManager()
|
private val rightTabbed = TransportTabbed(transferManager)
|
||||||
private val leftTabbed = TransportTabbed(transferManager, leftTransferManager)
|
private val leftTransferManager = createInternalTransferManager(leftTabbed, rightTabbed)
|
||||||
private val rightTabbed = TransportTabbed(transferManager, rightTransferManager)
|
private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed)
|
||||||
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
|
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
@@ -56,16 +39,12 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
isFocusable = false
|
isFocusable = false
|
||||||
|
|
||||||
|
leftTabbed.internalTransferManager = leftTransferManager
|
||||||
|
rightTabbed.internalTransferManager = rightTransferManager
|
||||||
|
|
||||||
leftTabbed.addLocalTab()
|
leftTabbed.addLocalTab()
|
||||||
rightTabbed.addSelectionTab()
|
rightTabbed.addSelectionTab()
|
||||||
|
|
||||||
leftTransferManager.source = leftTabbed
|
|
||||||
leftTransferManager.target = rightTabbed
|
|
||||||
|
|
||||||
rightTransferManager.source = rightTabbed
|
|
||||||
rightTransferManager.target = leftTabbed
|
|
||||||
|
|
||||||
|
|
||||||
val scrollPane = JScrollPane(transferTable)
|
val scrollPane = JScrollPane(transferTable)
|
||||||
scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
@@ -103,6 +82,36 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
Disposer.register(this, rightTabbed)
|
Disposer.register(this, rightTabbed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createInternalTransferManager(
|
||||||
|
source: TransportTabbed,
|
||||||
|
target: TransportTabbed
|
||||||
|
): InternalTransferManager {
|
||||||
|
return DefaultInternalTransferManager(
|
||||||
|
{ owner },
|
||||||
|
coroutineScope,
|
||||||
|
transferManager,
|
||||||
|
object : DefaultInternalTransferManager.WorkdirProvider {
|
||||||
|
override fun getWorkdir(): Path? {
|
||||||
|
return source.getSelectedTransportPanel()?.workdir
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTableModel(): TransportTableModel? {
|
||||||
|
return source.getSelectedTransportPanel()?.getTableModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
object : DefaultInternalTransferManager.WorkdirProvider {
|
||||||
|
override fun getWorkdir(): Path? {
|
||||||
|
return target.getSelectedTransportPanel()?.workdir
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTableModel(): TransportTableModel? {
|
||||||
|
return target.getSelectedTransportPanel()?.getTableModel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fun getTransferManager(): TransferManager {
|
fun getTransferManager(): TransferManager {
|
||||||
return transferManager
|
return transferManager
|
||||||
}
|
}
|
||||||
@@ -115,375 +124,4 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return rightTabbed
|
return rightTabbed
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class AskTransfer(
|
|
||||||
val option: Int,
|
|
||||||
val action: TransferAction,
|
|
||||||
val applyAll: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class AskTransferContext(var action: TransferAction, var applyAll: Boolean)
|
|
||||||
|
|
||||||
private inner class MyInternalTransferManager() : InternalTransferManager {
|
|
||||||
lateinit var source: TransportTabbed
|
|
||||||
lateinit var target: TransportTabbed
|
|
||||||
|
|
||||||
override fun canTransfer(paths: List<Path>): Boolean {
|
|
||||||
return target.getSelectedTransportPanel()?.workdir != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTransfer(
|
|
||||||
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
|
||||||
mode: TransferMode
|
|
||||||
): CompletableFuture<Unit> {
|
|
||||||
val workdir = (if (mode == TransferMode.Delete || mode == TransferMode.ChangePermission)
|
|
||||||
source.getSelectedTransportPanel()?.workdir else target.getSelectedTransportPanel()?.workdir)
|
|
||||||
?: throw IllegalStateException()
|
|
||||||
return addTransfer(paths, workdir, mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTransfer(
|
|
||||||
paths: List<Pair<Path, TransportTableModel.Attributes>>,
|
|
||||||
targetWorkdir: Path,
|
|
||||||
mode: TransferMode
|
|
||||||
): CompletableFuture<Unit> {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
|
|
||||||
if (paths.isEmpty()) return CompletableFuture.completedFuture(Unit)
|
|
||||||
|
|
||||||
val future = CompletableFuture<Unit>()
|
|
||||||
val panel = getTransportPanel(targetWorkdir.fileSystem, leftTabbed)
|
|
||||||
?: getTransportPanel(targetWorkdir.fileSystem, rightTabbed)
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val context = AskTransferContext(TransferAction.Overwrite, false)
|
|
||||||
for (pair in paths) {
|
|
||||||
if (mode == TransferMode.Transfer && panel != null) {
|
|
||||||
val action = withContext(Dispatchers.Swing) {
|
|
||||||
getTransferAction(context, panel, pair.second)
|
|
||||||
}
|
|
||||||
if (action == null) {
|
|
||||||
break
|
|
||||||
} else if (context.applyAll) {
|
|
||||||
if (action == TransferAction.Skip) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (action == TransferAction.Skip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val flag = doAddTransfer(targetWorkdir, pair, mode, context.action, future)
|
|
||||||
if (flag != FileVisitResult.CONTINUE) break
|
|
||||||
}
|
|
||||||
future.complete(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) log.error(e.message, e)
|
|
||||||
future.completeExceptionally(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addHighTransfer(source: Path, target: Path): String {
|
|
||||||
val transfer = FileTransfer(
|
|
||||||
parentId = StringUtils.EMPTY,
|
|
||||||
source = source,
|
|
||||||
target = target,
|
|
||||||
size = Files.size(source),
|
|
||||||
action = TransferAction.Overwrite,
|
|
||||||
priority = Transfer.Priority.High
|
|
||||||
)
|
|
||||||
if (transferManager.addTransfer(transfer)) {
|
|
||||||
return transfer.id()
|
|
||||||
} else {
|
|
||||||
throw IllegalStateException("Cannot add high transfer.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTransferListener(listener: TransferListener): Disposable {
|
|
||||||
return transferManager.addTransferListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTransferAction(
|
|
||||||
context: AskTransferContext,
|
|
||||||
panel: TransportPanel,
|
|
||||||
source: TransportTableModel.Attributes
|
|
||||||
): TransferAction? {
|
|
||||||
if (context.applyAll) return context.action
|
|
||||||
|
|
||||||
val model = panel.getTableModel()
|
|
||||||
for (i in 0 until model.rowCount) {
|
|
||||||
val c = model.getAttributes(i)
|
|
||||||
if (c.name != source.name) continue
|
|
||||||
val transfer = askTransfer(source, c)
|
|
||||||
context.action = transfer.action
|
|
||||||
context.applyAll = transfer.applyAll
|
|
||||||
if (transfer.option != JOptionPane.OK_OPTION) return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.action
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getTransportPanel(fileSystem: FileSystem, tabbed: TransportTabbed): TransportPanel? {
|
|
||||||
for (i in 0 until tabbed.tabCount) {
|
|
||||||
val c = tabbed.getComponentAt(i)
|
|
||||||
if (c is TransportPanel) {
|
|
||||||
if (c.loader.isLoaded) {
|
|
||||||
if (c.loader.get().fileSystem == fileSystem) {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun askTransfer(
|
|
||||||
source: TransportTableModel.Attributes,
|
|
||||||
target: TransportTableModel.Attributes
|
|
||||||
): AskTransfer {
|
|
||||||
val formMargin = "7dlu"
|
|
||||||
val layout = FormLayout(
|
|
||||||
"left:pref, $formMargin, default:grow, 2dlu, left:pref",
|
|
||||||
"pref, 12dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
|
||||||
)
|
|
||||||
|
|
||||||
val iconSize = 36
|
|
||||||
// @formatter:off
|
|
||||||
val targetIcon = ScaleIcon(if(target.isDirectory) NativeIcons.folderIcon else NativeIcons.fileIcon, iconSize)
|
|
||||||
val sourceIcon = ScaleIcon(if(source.isDirectory) NativeIcons.folderIcon else NativeIcons.fileIcon, iconSize)
|
|
||||||
val sourceModified= DateFormatUtils.format(Date(source.lastModifiedTime), I18n.getString("termora.date-format"))
|
|
||||||
val targetModified= DateFormatUtils.format(Date(target.lastModifiedTime), I18n.getString("termora.date-format"))
|
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
|
|
||||||
val actionsComBoBox = JComboBox<TransferAction>()
|
|
||||||
actionsComBoBox.addItem(TransferAction.Overwrite)
|
|
||||||
actionsComBoBox.addItem(TransferAction.Append)
|
|
||||||
actionsComBoBox.addItem(TransferAction.Skip)
|
|
||||||
actionsComBoBox.renderer = object : DefaultListCellRenderer() {
|
|
||||||
override fun getListCellRendererComponent(
|
|
||||||
list: JList<*>?,
|
|
||||||
value: Any?,
|
|
||||||
index: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
cellHasFocus: Boolean
|
|
||||||
): Component {
|
|
||||||
var text = value?.toString() ?: StringUtils.EMPTY
|
|
||||||
if (value == TransferAction.Overwrite) {
|
|
||||||
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
|
||||||
} else if (value == TransferAction.Skip) {
|
|
||||||
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
|
||||||
} else if (value == TransferAction.Append) {
|
|
||||||
text = I18n.getString("termora.transport.sftp.already-exists.append")
|
|
||||||
}
|
|
||||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val applyAllCheckbox = JCheckBox(I18n.getString("termora.transport.sftp.already-exists.apply-all"))
|
|
||||||
val box = Box.createHorizontalBox()
|
|
||||||
box.add(actionsComBoBox)
|
|
||||||
box.add(Box.createHorizontalStrut(8))
|
|
||||||
box.add(applyAllCheckbox)
|
|
||||||
box.add(Box.createHorizontalGlue())
|
|
||||||
|
|
||||||
val ttBox = Box.createVerticalBox()
|
|
||||||
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message1")))
|
|
||||||
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message2")))
|
|
||||||
|
|
||||||
val warningIcon = ScaleIcon(Icons.warningIntroduction, iconSize)
|
|
||||||
|
|
||||||
var rows = 1
|
|
||||||
val step = 2
|
|
||||||
val panel = FormBuilder.create().layout(layout)
|
|
||||||
// tip
|
|
||||||
.add(JLabel(warningIcon)).xy(1, rows)
|
|
||||||
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
|
||||||
// name
|
|
||||||
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
|
||||||
.add(source.name).xyw(3, rows, 3).apply { rows += step }
|
|
||||||
// separator
|
|
||||||
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
|
||||||
// Destination
|
|
||||||
.add("${I18n.getString("termora.transport.sftp.already-exists.destination")}:").xy(1, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
// Folder
|
|
||||||
.add(JLabel(targetIcon)).xy(1, rows, "center, fill")
|
|
||||||
.add(targetModified).xyw(3, rows, 3).apply { rows += step }
|
|
||||||
// Source
|
|
||||||
.add("${I18n.getString("termora.transport.sftp.already-exists.source")}:").xy(1, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
// Folder
|
|
||||||
.add(JLabel(sourceIcon)).xy(1, rows, "center, fill")
|
|
||||||
.add(sourceModified).xyw(3, rows, 3).apply { rows += step }
|
|
||||||
// separator
|
|
||||||
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
|
||||||
// name
|
|
||||||
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.actions")}:")).xy(1, rows)
|
|
||||||
.add(box).xyw(3, rows, 3).apply { rows += step }
|
|
||||||
.build()
|
|
||||||
panel.putClientProperty("SKIP_requestFocusInWindow", true)
|
|
||||||
|
|
||||||
return AskTransfer(
|
|
||||||
option = OptionPane.showConfirmDialog(
|
|
||||||
owner, panel,
|
|
||||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
title = source.name,
|
|
||||||
initialValue = JOptionPane.OK_OPTION,
|
|
||||||
) {
|
|
||||||
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
|
||||||
it.setLocationRelativeTo(it.owner)
|
|
||||||
},
|
|
||||||
action = actionsComBoBox.selectedItem as TransferAction,
|
|
||||||
applyAll = applyAllCheckbox.isSelected
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doAddTransfer(
|
|
||||||
workdir: Path,
|
|
||||||
pair: Pair<Path, TransportTableModel.Attributes>,
|
|
||||||
mode: TransferMode,
|
|
||||||
action: TransferAction,
|
|
||||||
future: CompletableFuture<Unit>
|
|
||||||
): FileVisitResult {
|
|
||||||
|
|
||||||
val isDirectory = pair.second.isDirectory
|
|
||||||
val path = pair.first
|
|
||||||
if (isDirectory.not()) {
|
|
||||||
val transfer = createTransfer(path, workdir.resolve(path.name), false, StringUtils.EMPTY, mode, action)
|
|
||||||
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
val continued = AtomicBoolean(true)
|
|
||||||
val queue = ArrayDeque<Transfer>()
|
|
||||||
val isCancelled =
|
|
||||||
{ (future.isCancelled || future.isCompletedExceptionally).apply { continued.set(this.not()) } }
|
|
||||||
val basedir = if (isDirectory) workdir.resolve(path.name) else workdir
|
|
||||||
val visitor = object : FileVisitor<Path> {
|
|
||||||
override fun preVisitDirectory(
|
|
||||||
dir: Path,
|
|
||||||
attrs: BasicFileAttributes
|
|
||||||
): FileVisitResult {
|
|
||||||
val parentId = queue.lastOrNull()?.id() ?: StringUtils.EMPTY
|
|
||||||
// @formatter:off
|
|
||||||
val transfer = when (mode) {
|
|
||||||
TransferMode.Delete -> createTransfer(dir, dir, true, parentId, mode, action)
|
|
||||||
TransferMode.ChangePermission -> createTransfer(path, dir, true, parentId, mode, action, pair.second.permissions)
|
|
||||||
else -> createTransfer(dir, basedir.resolve(path.relativize(dir).pathString), true, parentId, mode, action)
|
|
||||||
}
|
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
queue.addLast(transfer)
|
|
||||||
|
|
||||||
if (transferManager.addTransfer(transfer).not()) {
|
|
||||||
continued.set(false)
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFile(
|
|
||||||
file: Path,
|
|
||||||
attrs: BasicFileAttributes
|
|
||||||
): FileVisitResult {
|
|
||||||
|
|
||||||
val parentId = queue.last().id()
|
|
||||||
// @formatter:off
|
|
||||||
val transfer = when (mode) {
|
|
||||||
TransferMode.Delete -> createTransfer(file, file, false, parentId, mode, action)
|
|
||||||
TransferMode.ChangePermission -> createTransfer(file, file, false, parentId, mode, action, pair.second.permissions)
|
|
||||||
else -> createTransfer(file, basedir.resolve(path.relativize(file).pathString), false, parentId, mode, action)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferManager.addTransfer(transfer).not()) {
|
|
||||||
continued.set(false)
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFileFailed(
|
|
||||||
file: Path?,
|
|
||||||
exc: IOException
|
|
||||||
): FileVisitResult {
|
|
||||||
if (log.isErrorEnabled) log.error(exc.message, exc)
|
|
||||||
future.completeExceptionally(exc)
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postVisitDirectory(
|
|
||||||
dir: Path?,
|
|
||||||
exc: IOException?
|
|
||||||
): FileVisitResult {
|
|
||||||
val c = queue.removeLast()
|
|
||||||
if (c is TransferScanner) c.scanned()
|
|
||||||
return if (isCancelled.invoke()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
PathWalker.walkFileTree(path, visitor)
|
|
||||||
|
|
||||||
// 已经添加的则继续传输
|
|
||||||
while (queue.isNotEmpty()) {
|
|
||||||
val c = queue.removeLast()
|
|
||||||
if (c is TransferScanner) c.scanned()
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (continued.get()) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun createTransfer(
|
|
||||||
source: Path,
|
|
||||||
target: Path,
|
|
||||||
isDirectory: Boolean,
|
|
||||||
parentId: String,
|
|
||||||
mode: TransferMode,
|
|
||||||
action:TransferAction,
|
|
||||||
permissions: Set<PosixFilePermission>? = null
|
|
||||||
): Transfer {
|
|
||||||
if (mode == TransferMode.Delete) {
|
|
||||||
return DeleteTransfer(
|
|
||||||
parentId,
|
|
||||||
source,
|
|
||||||
isDirectory,
|
|
||||||
if (isDirectory) 1 else Files.size(source)
|
|
||||||
)
|
|
||||||
} else if (mode == TransferMode.ChangePermission) {
|
|
||||||
if (permissions == null) throw IllegalStateException()
|
|
||||||
return ChangePermissionTransfer(
|
|
||||||
parentId,
|
|
||||||
target,
|
|
||||||
isDirectory = isDirectory,
|
|
||||||
permissions = permissions,
|
|
||||||
size = if (isDirectory) 1 else Files.size(target)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectory) {
|
|
||||||
return DirectoryTransfer(
|
|
||||||
parentId = parentId,
|
|
||||||
source = source,
|
|
||||||
target = target,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileTransfer(
|
|
||||||
parentId = parentId,
|
|
||||||
source = source,
|
|
||||||
target = target,
|
|
||||||
action = action,
|
|
||||||
size = Files.size(source)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.transfer.internal.sftp
|
package app.termora.transfer.internal.sftp
|
||||||
|
|
||||||
import app.termora.SshClients
|
import app.termora.SshClients
|
||||||
import app.termora.database.DatabaseManager
|
|
||||||
import app.termora.protocol.PathHandler
|
import app.termora.protocol.PathHandler
|
||||||
import app.termora.protocol.PathHandlerRequest
|
import app.termora.protocol.PathHandlerRequest
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
@@ -14,7 +13,6 @@ import org.apache.sshd.sftp.client.SftpClientFactory
|
|||||||
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { SFTPTransferProtocolProvider() }
|
val instance by lazy { SFTPTransferProtocolProvider() }
|
||||||
private val sftp get() = DatabaseManager.getInstance().sftp
|
|
||||||
const val PROTOCOL = "sftp"
|
const val PROTOCOL = "sftp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
src/main/resources/icons/questionMark.svg
Normal file
6
src/main/resources/icons/questionMark.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
|
||||||
|
<path d="M7.59245 8.521C7.67312 8.33767 7.77212 8.18 7.88945 8.048C8.00679 7.916 8.16445 7.75833 8.36245 7.575C8.55312 7.40267 8.70345 7.25417 8.81345 7.1295C8.92712 7.00483 9.02245 6.86 9.09945 6.695C9.17645 6.52633 9.21495 6.33933 9.21495 6.134C9.21495 5.903 9.16545 5.69767 9.06645 5.518C8.97112 5.33833 8.83545 5.199 8.65945 5.1C8.48345 5.001 8.28179 4.9515 8.05445 4.9515C7.80145 4.9515 7.57779 5.0065 7.38345 5.1165C7.18912 5.2265 7.03879 5.3805 6.93245 5.5785C6.90638 5.62705 6.88351 5.67735 6.86383 5.72943C6.75806 6.00931 6.5304 6.2605 6.2312 6.2605V6.2605C5.932 6.2605 5.68157 6.01527 5.74167 5.72217C5.78989 5.48707 5.87148 5.27051 5.98645 5.0725C6.18445 4.7315 6.46312 4.4675 6.82245 4.2805C7.18179 4.0935 7.59612 4 8.06545 4C8.51279 4 8.90695 4.08617 9.24795 4.2585C9.58895 4.43083 9.85295 4.67467 10.04 4.99C10.227 5.30167 10.3186 5.661 10.315 6.068C10.315 6.38333 10.2655 6.662 10.1665 6.904C10.0675 7.14233 9.94645 7.34217 9.80345 7.5035C9.66045 7.66483 9.47529 7.84633 9.24795 8.048C9.07929 8.19833 8.94729 8.32483 8.85195 8.4275C8.75662 8.53017 8.67595 8.64567 8.60995 8.774C8.59197 8.80996 8.57743 8.84636 8.56569 8.88312C8.47784 9.15799 8.28252 9.401 7.99395 9.401V9.401C7.70538 9.401 7.45381 9.16525 7.4934 8.87941C7.51026 8.75764 7.54001 8.6378 7.59245 8.521Z" fill="#6C707E"/>
|
||||||
|
<circle cx="8" cy="10.75" r="0.75" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
6
src/main/resources/icons/questionMark_dark.svg
Normal file
6
src/main/resources/icons/questionMark_dark.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
|
||||||
|
<path d="M7.59245 8.521C7.67312 8.33767 7.77212 8.18 7.88945 8.048C8.00679 7.916 8.16445 7.75833 8.36245 7.575C8.55312 7.40267 8.70345 7.25417 8.81345 7.1295C8.92712 7.00483 9.02245 6.86 9.09945 6.695C9.17645 6.52633 9.21495 6.33933 9.21495 6.134C9.21495 5.903 9.16545 5.69767 9.06645 5.518C8.97112 5.33833 8.83545 5.199 8.65945 5.1C8.48345 5.001 8.28179 4.9515 8.05445 4.9515C7.80145 4.9515 7.57779 5.0065 7.38345 5.1165C7.18912 5.2265 7.03879 5.3805 6.93245 5.5785C6.90638 5.62705 6.88351 5.67735 6.86383 5.72943C6.75806 6.00931 6.5304 6.2605 6.2312 6.2605V6.2605C5.932 6.2605 5.68157 6.01527 5.74167 5.72217C5.78989 5.48707 5.87148 5.27051 5.98645 5.0725C6.18445 4.7315 6.46312 4.4675 6.82245 4.2805C7.18179 4.0935 7.59612 4 8.06545 4C8.51279 4 8.90695 4.08617 9.24795 4.2585C9.58895 4.43083 9.85295 4.67467 10.04 4.99C10.227 5.30167 10.3186 5.661 10.315 6.068C10.315 6.38333 10.2655 6.662 10.1665 6.904C10.0675 7.14233 9.94645 7.34217 9.80345 7.5035C9.66045 7.66483 9.47529 7.84633 9.24795 8.048C9.07929 8.19833 8.94729 8.32483 8.85195 8.4275C8.75662 8.53017 8.67595 8.64567 8.60995 8.774C8.59197 8.80996 8.57743 8.84636 8.56569 8.88312C8.47784 9.15799 8.28252 9.401 7.99395 9.401V9.401C7.70538 9.401 7.45381 9.16525 7.4934 8.87941C7.51026 8.75764 7.54001 8.6378 7.59245 8.521Z" fill="#CED0D6"/>
|
||||||
|
<circle cx="8" cy="10.75" r="0.75" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
4
src/main/resources/icons/transferToolWindow.svg
Normal file
4
src/main/resources/icons/transferToolWindow.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.8536 3.14645C11.6583 2.95118 11.3417 2.95118 11.1464 3.14645L8.14645 6.14645C7.95118 6.34171 7.95118 6.65829 8.14645 6.85355C8.34171 7.04882 8.65829 7.04882 8.85355 6.85355L11 4.70711V12.5C11 12.7761 11.2239 13 11.5 13C11.7761 13 12 12.7761 12 12.5V4.70711L14.1464 6.85355C14.3417 7.04882 14.6583 7.04882 14.8536 6.85355C15.0488 6.65829 15.0488 6.34171 14.8536 6.14645L11.8536 3.14645Z" fill="#6C707E"/>
|
||||||
|
<path d="M5 3.5C5 3.22386 4.77614 3 4.5 3C4.22386 3 4 3.22386 4 3.5V11.2929L1.85355 9.14645C1.65829 8.95118 1.34171 8.95118 1.14645 9.14645C0.951184 9.34171 0.951184 9.65829 1.14645 9.85355L4.14645 12.8536C4.34171 13.0488 4.65829 13.0488 4.85355 12.8536L7.85355 9.85355C8.04882 9.65829 8.04882 9.34171 7.85355 9.14645C7.65829 8.95118 7.34171 8.95118 7.14645 9.14645L5 11.2929V3.5Z" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 919 B |
4
src/main/resources/icons/transferToolWindow_dark.svg
Normal file
4
src/main/resources/icons/transferToolWindow_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.8536 3.14645C11.6583 2.95118 11.3417 2.95118 11.1464 3.14645L8.14645 6.14645C7.95118 6.34171 7.95118 6.65829 8.14645 6.85355C8.34171 7.04882 8.65829 7.04882 8.85355 6.85355L11 4.70711V12.5C11 12.7761 11.2239 13 11.5 13C11.7761 13 12 12.7761 12 12.5V4.70711L14.1464 6.85355C14.3417 7.04882 14.6583 7.04882 14.8536 6.85355C15.0488 6.65829 15.0488 6.34171 14.8536 6.14645L11.8536 3.14645Z" fill="#CED0D6"/>
|
||||||
|
<path d="M5 3.5C5 3.22386 4.77614 3 4.5 3C4.22386 3 4 3.22386 4 3.5V11.2929L1.85355 9.14645C1.65829 8.95118 1.34171 8.95118 1.14645 9.14645C0.951184 9.34171 0.951184 9.65829 1.14645 9.85355L4.14645 12.8536C4.34171 13.0488 4.65829 13.0488 4.85355 12.8536L7.85355 9.85355C8.04882 9.65829 8.04882 9.34171 7.85355 9.14645C7.65829 8.95118 7.34171 8.95118 7.14645 9.14645L5 11.2929V3.5Z" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 919 B |
Reference in New Issue
Block a user