mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02: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"
|
||||
}
|
||||
|
||||
|
||||
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