mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22: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 eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_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 vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_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 codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_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 applyNotConflictsLeft by lazy {
|
||||
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.panel.vw.NvidiaSMIVisualWindow
|
||||
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.ui.FlatRoundBorder
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -118,6 +119,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Transfer
|
||||
add(initTransferActionButton())
|
||||
|
||||
// Snippet
|
||||
add(initSnippetActionButton())
|
||||
|
||||
@@ -185,6 +189,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
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 {
|
||||
val btn = JButton(Icons.codeSpan)
|
||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||
|
||||
@@ -588,6 +588,7 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
||||
requestFocusInWindow()
|
||||
}
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
||||
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
||||
for (name in windows.split(",")) {
|
||||
@@ -605,6 +606,13 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
||||
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()
|
||||
}
|
||||
|
||||
override fun toolbarButtons(): List<JButton> {
|
||||
return listOf(percentageBtn)
|
||||
override fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||
return listOf(percentageBtn to Position.Right)
|
||||
}
|
||||
|
||||
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) :
|
||||
JPanel(BorderLayout()), VisualWindow {
|
||||
|
||||
protected enum class Position {
|
||||
Left, Right
|
||||
}
|
||||
|
||||
private val stickPx = 2
|
||||
protected val properties get() = DatabaseManager.getInstance().properties
|
||||
private val titleLabel = JLabel()
|
||||
@@ -90,7 +94,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
alwaysTopBtn.isVisible = false
|
||||
}
|
||||
|
||||
protected open fun toolbarButtons(): List<JButton> {
|
||||
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@@ -164,19 +168,19 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
|
||||
private fun initToolBar() {
|
||||
val btns = toolbarButtons()
|
||||
val count = 2 + btns.size
|
||||
val buttons = toolbarButtons()
|
||||
val count = 2 + buttons.size - (buttons.count { it.second == Position.Left } * 2)
|
||||
toolbar.add(alwaysTopBtn)
|
||||
toolbar.add(Box.createHorizontalStrut(count * 26))
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
buttons.filter { it.second == Position.Left }.forEach { toolbar.add(it.first) }
|
||||
toolbar.add(Box.createHorizontalStrut(max(count * 26, 0)))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(titleLabel)
|
||||
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(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)
|
||||
add(toolbar, BorderLayout.NORTH)
|
||||
}
|
||||
@@ -318,6 +322,9 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(this) }
|
||||
}
|
||||
|
||||
protected open fun beforeClose(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
protected open fun close() {
|
||||
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.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
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 owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
private val disposed = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -153,7 +154,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
private fun refreshView() {
|
||||
coroutineScope.launch(Dispatchers.Swing) {
|
||||
val timeout = 500
|
||||
while (coroutineScope.isActive) {
|
||||
while (coroutineScope.isActive && disposed.get().not()) {
|
||||
for (row in 0 until rowCount) {
|
||||
val treePath = getPathForRow(row) ?: 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 var progress = 0.0
|
||||
private var progressInt = 0
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
||||
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.InterruptedIOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -114,7 +115,7 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
val result = AtomicBoolean(false)
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
if (validGrandfather(node.transfer.parentId())) {
|
||||
map[node.transfer.id()] = node
|
||||
putNodeToMap(node.transfer.id(), node)
|
||||
insertNodeInto(node, parent, parent.childCount)
|
||||
result.set(true)
|
||||
}
|
||||
@@ -124,6 +125,21 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
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)
|
||||
// 移除
|
||||
map.remove(node.transfer.id())
|
||||
removeNodeFromMap(node.transfer.id())
|
||||
removeNodeFromParent(node)
|
||||
|
||||
// 如果删除时还在传输,那么需要减去大小
|
||||
@@ -388,6 +404,10 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
lock.withLock { condition.signalAll() }
|
||||
} catch (_: CancellationException) {
|
||||
break
|
||||
} catch (_: InterruptedException) {
|
||||
break
|
||||
} catch (_: InterruptedIOException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -46,7 +45,6 @@ import java.nio.file.attribute.PosixFilePermissions
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.regex.Pattern
|
||||
@@ -120,7 +118,7 @@ class TransportPanel(
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
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 defaultPath by lazy { getSupport().path }
|
||||
@@ -456,6 +454,9 @@ class TransportPanel(
|
||||
|
||||
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||
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()
|
||||
|
||||
if (loading) return false
|
||||
@@ -663,7 +664,7 @@ class TransportPanel(
|
||||
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
|
||||
|
||||
@@ -854,7 +855,6 @@ class TransportPanel(
|
||||
futures.forEach { it.cancel(true) }
|
||||
futures.clear()
|
||||
coroutineScope.cancel()
|
||||
if (loader.isLoaded && _fileSystem.isOpen) IOUtils.closeQuietly(_fileSystem)
|
||||
loadingPanel.busyLabel.isBusy = false
|
||||
}
|
||||
}
|
||||
@@ -1038,10 +1038,12 @@ class TransportPanel(
|
||||
val path = files.first().first
|
||||
processPath(path.name) {
|
||||
if (c.includeSubFolder) {
|
||||
val future = transferManager.addTransfer(
|
||||
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
||||
InternalTransferManager.TransferMode.ChangePermission
|
||||
)
|
||||
val future = withContext(Dispatchers.Swing) {
|
||||
transferManager.addTransfer(
|
||||
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
||||
InternalTransferManager.TransferMode.ChangePermission
|
||||
)
|
||||
}
|
||||
mountFuture(future)
|
||||
future.get()
|
||||
} else {
|
||||
@@ -1064,7 +1066,7 @@ class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun processPath(name: String, action: () -> Unit) {
|
||||
private fun processPath(name: String, action: suspend () -> Unit) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
action.invoke()
|
||||
|
||||
@@ -18,10 +18,10 @@ import javax.swing.SwingUtilities
|
||||
@Suppress("DuplicatedCode")
|
||||
class TransportTabbed(
|
||||
private val transferManager: TransferManager,
|
||||
private val internalTransferManager: InternalTransferManager
|
||||
) : FlatTabbedPane(), Disposable {
|
||||
private val addBtn = JButton(Icons.add)
|
||||
private val tabbed get() = this
|
||||
lateinit var internalTransferManager: InternalTransferManager
|
||||
|
||||
init {
|
||||
initViews()
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
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.transfer.InternalTransferManager.TransferMode
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.io.IOException
|
||||
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 java.nio.file.Path
|
||||
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 {
|
||||
@@ -41,10 +24,10 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
private val splitPane = JSplitPane()
|
||||
private val transferManager = TransferTableModel(coroutineScope)
|
||||
private val transferTable = TransferTable(coroutineScope, transferManager)
|
||||
private val leftTransferManager = MyInternalTransferManager()
|
||||
private val rightTransferManager = MyInternalTransferManager()
|
||||
private val leftTabbed = TransportTabbed(transferManager, leftTransferManager)
|
||||
private val rightTabbed = TransportTabbed(transferManager, rightTransferManager)
|
||||
private val leftTabbed = TransportTabbed(transferManager)
|
||||
private val rightTabbed = TransportTabbed(transferManager)
|
||||
private val leftTransferManager = createInternalTransferManager(leftTabbed, rightTabbed)
|
||||
private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed)
|
||||
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
@@ -56,16 +39,12 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
private fun initView() {
|
||||
isFocusable = false
|
||||
|
||||
leftTabbed.internalTransferManager = leftTransferManager
|
||||
rightTabbed.internalTransferManager = rightTransferManager
|
||||
|
||||
leftTabbed.addLocalTab()
|
||||
rightTabbed.addSelectionTab()
|
||||
|
||||
leftTransferManager.source = leftTabbed
|
||||
leftTransferManager.target = rightTabbed
|
||||
|
||||
rightTransferManager.source = rightTabbed
|
||||
rightTransferManager.target = leftTabbed
|
||||
|
||||
|
||||
val scrollPane = JScrollPane(transferTable)
|
||||
scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
@@ -103,6 +82,36 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
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 {
|
||||
return transferManager
|
||||
}
|
||||
@@ -115,375 +124,4 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
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
|
||||
|
||||
import app.termora.SshClients
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
@@ -14,7 +13,6 @@ import org.apache.sshd.sftp.client.SftpClientFactory
|
||||
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
||||
companion object {
|
||||
val instance by lazy { SFTPTransferProtocolProvider() }
|
||||
private val sftp get() = DatabaseManager.getInstance().sftp
|
||||
const val PROTOCOL = "sftp"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user