diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 841c9d5..0b89078 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -73,6 +73,9 @@ class ApplicationRunner { // 解密数据 val openDoor = measureTimeMillis { openDoor() } + // clear temporary + clearTemporary() + // 启动主窗口 val startMainFrame = measureTimeMillis { startMainFrame() } @@ -94,6 +97,22 @@ class ApplicationRunner { } } + @Suppress("OPT_IN_USAGE") + private fun clearTemporary() { + GlobalScope.launch(Dispatchers.IO) { + + // 启动时清除 + FileUtils.cleanDirectory(File(Application.getBaseDataDir(), "temporary")) + + // 关闭时清除 + Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable { + override fun dispose() { + FileUtils.cleanDirectory(File(Application.getBaseDataDir(), "temporary")) + } + }) + } + + } private fun openDoor() { if (Doorman.getInstance().isWorking()) { diff --git a/src/main/kotlin/app/termora/transport/FileSystemPanel.kt b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt index 466d4a6..0f464fc 100644 --- a/src/main/kotlin/app/termora/transport/FileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/FileSystemPanel.kt @@ -12,6 +12,7 @@ import com.formdev.flatlaf.util.SystemInfo import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing import org.apache.commons.io.FileUtils +import org.apache.commons.io.file.PathUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.exception.ExceptionUtils @@ -35,8 +36,11 @@ import java.nio.file.* import java.util.* import javax.swing.* import javax.swing.table.DefaultTableCellRenderer +import kotlin.io.path.absolutePathString import kotlin.io.path.exists +import kotlin.io.path.getLastModifiedTime import kotlin.io.path.isDirectory +import kotlin.time.Duration.Companion.milliseconds /** @@ -44,9 +48,8 @@ import kotlin.io.path.isDirectory */ class FileSystemPanel( private val fileSystem: FileSystem, - private val transportManager: TransportManager, private val host: Host -) : JPanel(BorderLayout()), Disposable, FileSystemTransportListener.Provider { +) : JPanel(BorderLayout()), Disposable { companion object { private val log = LoggerFactory.getLogger(FileSystemPanel::class.java) @@ -64,6 +67,12 @@ class FileSystemPanel( private val showHiddenFilesBtn = JButton(Icons.eyeClose) private val properties get() = Database.getDatabase().properties private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" } + private val evt by lazy { AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) } + + /** + * Edit + */ + private val coroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) } val workdir get() = tableModel.workdir @@ -342,6 +351,9 @@ class FileSystemPanel( } + override fun dispose() { + coroutineScope.cancel() + } private fun copyLocalFileToFileSystem(files: List) { val event = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) @@ -425,14 +437,6 @@ class FileSystemPanel( } - override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { - listenerList.add(FileSystemTransportListener::class.java, listener) - } - - override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { - listenerList.remove(FileSystemTransportListener::class.java, listener) - } - private fun openFolder() { val row = table.selectedRow if (row < 0) return @@ -460,6 +464,7 @@ class FileSystemPanel( private fun showContextMenu(rows: IntArray, event: MouseEvent) { + val paths = rows.filter { it != 0 }.map { tableModel.getCacheablePath(it) } val popupMenu = FlatPopupMenu() val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new")) @@ -477,11 +482,22 @@ class FileSystemPanel( // 传输 val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer")) transfer.addActionListener { - val paths = rows.filter { it != 0 }.map { tableModel.getCacheablePath(it) } if (paths.isNotEmpty()) { transport(paths) } } + + // 编辑 + val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit")) + // 不是 Linux & 不是本地文件系统 & 包含文件 + edit.isEnabled = !SystemInfo.isLinux && !tableModel.isLocalFileSystem && paths.any { !it.isDirectory } + edit.addActionListener { + val files = paths.filter { !it.isDirectory } + if (files.isNotEmpty()) { + editFiles(files) + } + } + popupMenu.addSeparator() // 复制路径 @@ -574,6 +590,75 @@ class FileSystemPanel( popupMenu.show(table, event.x, event.y) } + private fun editFiles(files: List) { + if (files.isEmpty()) return + val transportManager = evt.getData(TransportDataProviders.TransportManager) ?: return + + val temporary = Paths.get(Application.getBaseDataDir().absolutePath, "temporary") + Files.createDirectories(temporary) + + for (file in files) { + val dir = Files.createTempDirectory(temporary, "termora-") + val path = Paths.get(dir.absolutePathString(), file.fileName) + transportManager.addTransport( + transport = FileTransport( + name = file.fileName, + source = file.path, + target = path, + sourceHolder = this, + targetHolder = this, + listener = editFileTransportListener(file.path, path) + ) + ) + } + } + + private fun editFileTransportListener(source: Path, localPath: Path): TransportListener { + return object : TransportListener { + override fun onTransportChanged(transport: Transport) { + // 传输成功 + if (transport.state == TransportState.Done) { + val transportManager = evt.getData(TransportDataProviders.TransportManager) ?: return + var lastModifiedTime = localPath.getLastModifiedTime().toMillis() + + if (SystemInfo.isMacOS) { + ProcessBuilder("open", "-a", "TextEdit", localPath.absolutePathString()).start() + } else if (SystemInfo.isWindows) { + ProcessBuilder("notepad", localPath.absolutePathString()).start() + } else { + return + } + + coroutineScope.launch(Dispatchers.IO) { + while (coroutineScope.isActive) { + try { + val nowModifiedTime = localPath.getLastModifiedTime().toMillis() + if (nowModifiedTime != lastModifiedTime) { + lastModifiedTime = nowModifiedTime + // upload + transportManager.addTransport( + transport = FileTransport( + name = PathUtils.getFileNameString(localPath.fileName), + source = localPath, + target = source, + sourceHolder = this@FileSystemPanel, + targetHolder = this@FileSystemPanel, + ) + ) + } + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + break + } + delay(250.milliseconds) + } + } + } + } + } + } @OptIn(DelicateCoroutinesApi::class) private fun renamePath(path: Path) { @@ -789,17 +874,31 @@ class FileSystemPanel( private suspend fun doTransport(paths: List) { if (paths.isEmpty()) return - - val listeners = listenerList.getListeners(FileSystemTransportListener::class.java) - if (listeners.isEmpty()) return - + val transportPanel = evt.getData(TransportDataProviders.TransportPanel) ?: return + val leftFileSystemPanel = evt.getData(TransportDataProviders.LeftFileSystemPanel) ?: return + val rightFileSystemPanel = evt.getData(TransportDataProviders.RightFileSystemPanel) ?: return + val sourceFileSystemPanel = this + val targetFileSystemPanel = if (this == leftFileSystemPanel) rightFileSystemPanel else leftFileSystemPanel // 收集数据 for (e in paths) { if (!e.isDirectory) { + val job = TransportJob( + fileSystemPanel = this, + workdir = workdir, + isDirectory = false, + path = e.path, + ) withContext(Dispatchers.Swing) { - listeners.forEach { it.transport(this@FileSystemPanel, workdir, false, e.path) } + transportPanel.transport( + sourceWorkdir = workdir, + targetWorkdir = targetFileSystemPanel.workdir, + isSourceDirectory = false, + sourcePath = e.path, + sourceHolder = sourceFileSystemPanel, + targetHolder = targetFileSystemPanel + ) } continue } @@ -811,12 +910,26 @@ class FileSystemPanel( val isDirectory = if (path.attributes != null) path.attributes.isDirectory else path.isDirectory() withContext(Dispatchers.Swing) { - listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) } + transportPanel.transport( + sourceWorkdir = workdir, + targetWorkdir = targetFileSystemPanel.workdir, + isSourceDirectory = isDirectory, + sourcePath = path, + sourceHolder = sourceFileSystemPanel, + targetHolder = targetFileSystemPanel + ) } } else { val isDirectory = path.isDirectory() withContext(Dispatchers.Swing) { - listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) } + transportPanel.transport( + sourceWorkdir = workdir, + targetWorkdir = targetFileSystemPanel.workdir, + isSourceDirectory = isDirectory, + sourcePath = path, + sourceHolder = sourceFileSystemPanel, + targetHolder = targetFileSystemPanel + ) } } } diff --git a/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt b/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt index 9c4170a..44790a4 100644 --- a/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt +++ b/src/main/kotlin/app/termora/transport/FileSystemTabbed.kt @@ -3,9 +3,9 @@ package app.termora.transport import app.termora.* import com.formdev.flatlaf.extras.components.FlatTabbedPane import org.apache.commons.lang3.StringUtils +import java.awt.Component import java.awt.Point import java.nio.file.FileSystems -import java.nio.file.Path import javax.swing.* import kotlin.math.max @@ -13,9 +13,8 @@ import kotlin.math.max class FileSystemTabbed( private val transportManager: TransportManager, private val isLeft: Boolean = false -) : FlatTabbedPane(), FileSystemTransportListener.Provider, Disposable { +) : FlatTabbedPane(), Disposable { private val addBtn = JButton(Icons.add) - private val listeners = mutableListOf() init { initView() @@ -36,23 +35,20 @@ class FileSystemTabbed( trailingComponent = toolbar if (isLeft) { - addFileSystemTransportProvider( - I18n.getString("termora.transport.local"), - FileSystemPanel( + addTab( + I18n.getString("termora.transport.local"), FileSystemPanel( FileSystems.getDefault(), - transportManager, host = Host( id = "local", name = I18n.getString("termora.transport.local"), protocol = Protocol.Local, ) - ).apply { reload() } - ) + ).apply { reload() }) setTabClosable(0, false) } else { - addFileSystemTransportProvider( + addTab( I18n.getString("termora.transport.sftp.select-host"), - SftpFileSystemPanel(transportManager) + SftpFileSystemPanel() ) } @@ -70,8 +66,8 @@ class FileSystemTabbed( dialog.isVisible = true for (host in dialog.hosts) { - val panel = SftpFileSystemPanel(transportManager, host) - addFileSystemTransportProvider(host.name, panel) + val panel = SftpFileSystemPanel(host) + addTab(host.name, panel) panel.connect() } @@ -120,9 +116,9 @@ class FileSystemTabbed( if (tabCount == 0) { if (!isLeft) { - addFileSystemTransportProvider( + addTab( I18n.getString("termora.transport.sftp.select-host"), - SftpFileSystemPanel(transportManager) + SftpFileSystemPanel() ) } } @@ -130,39 +126,31 @@ class FileSystemTabbed( } - fun addFileSystemTransportProvider(title: String, provider: FileSystemTransportListener.Provider) { - if (provider !is JComponent) { - throw IllegalArgumentException("Provider is not an JComponent") - } + override fun addTab(title: String, component: Component) { + super.addTab(title, component) - provider.addFileSystemTransportListener(object : FileSystemTransportListener { - override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { - listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) } - } - }) + selectedIndex = tabCount - 1 - // 修改 Tab名称 - provider.addPropertyChangeListener("TabName") { e -> - SwingUtilities.invokeLater { - val name = StringUtils.defaultIfEmpty( - e.newValue.toString(), - I18n.getString("termora.transport.sftp.select-host") - ) - for (i in 0 until tabCount) { - if (getComponentAt(i) == provider) { - setTitleAt(i, name) - break + if (component is SftpFileSystemPanel) { + component.addPropertyChangeListener("TabName") { e -> + SwingUtilities.invokeLater { + val name = StringUtils.defaultIfEmpty( + e.newValue.toString(), + I18n.getString("termora.transport.sftp.select-host") + ) + for (i in 0 until tabCount) { + if (getComponentAt(i) == component) { + setTitleAt(i, name) + break + } } } } } - addTab(title, provider) - - if (tabCount > 0) - selectedIndex = tabCount - 1 } + fun getSelectedFileSystemPanel(): FileSystemPanel? { return getFileSystemPanel(selectedIndex) } @@ -184,14 +172,6 @@ class FileSystemTabbed( return null } - override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { - listeners.add(listener) - } - - override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { - listeners.remove(listener) - } - override fun dispose() { while (tabCount > 0) { val c = getComponentAt(0) diff --git a/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt b/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt deleted file mode 100644 index 1bc414d..0000000 --- a/src/main/kotlin/app/termora/transport/FileSystemTransportListener.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.termora.transport - -import java.nio.file.Path -import java.util.* - -interface FileSystemTransportListener : EventListener { - /** - * @param workdir 当前工作目录 - * @param isDirectory 要传输的是否是文件夹 - * @param path 要传输的文件/文件夹 - */ - fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) - - - interface Provider { - fun addFileSystemTransportListener(listener: FileSystemTransportListener) - fun removeFileSystemTransportListener(listener: FileSystemTransportListener) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt index c871900..78b2d6a 100644 --- a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -21,15 +21,12 @@ import org.slf4j.LoggerFactory import java.awt.BorderLayout import java.awt.CardLayout import java.awt.event.ActionEvent -import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean import javax.swing.* class SftpFileSystemPanel( - private val transportManager: TransportManager, private var host: Host? = null -) : JPanel(BorderLayout()), Disposable, - FileSystemTransportListener.Provider { +) : JPanel(BorderLayout()), Disposable { companion object { private val log = LoggerFactory.getLogger(SftpFileSystemPanel::class.java) @@ -50,7 +47,6 @@ class SftpFileSystemPanel( private val connectingPanel = ConnectingPanel() private val selectHostPanel = SelectHostPanel() private val connectFailedPanel = ConnectFailedPanel() - private val listeners = mutableListOf() private val isDisposed = AtomicBoolean(false) private var client: SshClient? = null @@ -136,17 +132,7 @@ class SftpFileSystemPanel( withContext(Dispatchers.Swing) { state = State.Connected - val fileSystemPanel = FileSystemPanel(fileSystem, transportManager, host) - fileSystemPanel.addFileSystemTransportListener(object : FileSystemTransportListener { - override fun transport( - fileSystemPanel: FileSystemPanel, - workdir: Path, - isDirectory: Boolean, - path: Path - ) { - listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) } - } - }) + val fileSystemPanel = FileSystemPanel(fileSystem, host) cardPanel.add(fileSystemPanel, State.Connected.name) cardLayout.show(cardPanel, State.Connected.name) @@ -312,11 +298,4 @@ class SftpFileSystemPanel( } - override fun addFileSystemTransportListener(listener: FileSystemTransportListener) { - listeners.add(listener) - } - - override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) { - listeners.remove(listener) - } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/Transport.kt b/src/main/kotlin/app/termora/transport/Transport.kt index 4109b8a..9fa3963 100644 --- a/src/main/kotlin/app/termora/transport/Transport.kt +++ b/src/main/kotlin/app/termora/transport/Transport.kt @@ -31,10 +31,15 @@ abstract class Transport( val target: Path, val sourceHolder: Disposable, val targetHolder: Disposable, + val listener: TransportListener = TransportListener.EMPTY ) : Disposable, Runnable { private val listeners = ArrayList() + init { + listeners.add(listener) + } + @Volatile var state = TransportState.Waiting protected set(value) { @@ -142,9 +147,9 @@ private class SlidingWindowByteCounter { */ class FileTransport( name: String, source: Path, target: Path, - sourceHolder: Disposable, targetHolder: Disposable, + sourceHolder: Disposable, targetHolder: Disposable, listener: TransportListener = TransportListener.EMPTY ) : Transport( - name, source, target, sourceHolder, targetHolder, + name, source, target, sourceHolder, targetHolder, listener ), CopyStreamListener { companion object { diff --git a/src/main/kotlin/app/termora/transport/TransportJob.kt b/src/main/kotlin/app/termora/transport/TransportJob.kt new file mode 100644 index 0000000..eb02b30 --- /dev/null +++ b/src/main/kotlin/app/termora/transport/TransportJob.kt @@ -0,0 +1,27 @@ +package app.termora.transport + +import java.nio.file.Path + +data class TransportJob( + /** + * 发起方 + */ + val fileSystemPanel: FileSystemPanel, + /** + * 发起方工作目录 + */ + val workdir: Path, + /** + * 要传输的文件是否是文件夹 + */ + val isDirectory: Boolean, + /** + * 要传输的文件/文件夹 + */ + val path: Path, + + /** + * 监听 + */ + val listener: TransportListener? = null +) \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/TransportListener.kt b/src/main/kotlin/app/termora/transport/TransportListener.kt index 735ccda..2ba4f99 100644 --- a/src/main/kotlin/app/termora/transport/TransportListener.kt +++ b/src/main/kotlin/app/termora/transport/TransportListener.kt @@ -3,18 +3,33 @@ package app.termora.transport import java.util.* interface TransportListener : EventListener { + + companion object { + val EMPTY = object : TransportListener { + override fun onTransportAdded(transport: Transport) { + + } + + override fun onTransportRemoved(transport: Transport) { + } + + override fun onTransportChanged(transport: Transport) { + } + } + } + /** * Added */ - fun onTransportAdded(transport: Transport) + fun onTransportAdded(transport: Transport){} /** * Removed */ - fun onTransportRemoved(transport: Transport) + fun onTransportRemoved(transport: Transport){} /** * 状态变化 */ - fun onTransportChanged(transport: Transport) + fun onTransportChanged(transport: Transport){} } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/TransportPanel.kt b/src/main/kotlin/app/termora/transport/TransportPanel.kt index 32bbbe8..068b0bf 100644 --- a/src/main/kotlin/app/termora/transport/TransportPanel.kt +++ b/src/main/kotlin/app/termora/transport/TransportPanel.kt @@ -107,32 +107,6 @@ class TransportPanel : JPanel(BorderLayout()), Disposable, DataProvider { }) - leftFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener { - override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { - val target = rightFileSystemTabbed.getSelectedFileSystemPanel() ?: return - transport( - fileSystemPanel.workdir, target.workdir, - isSourceDirectory = isDirectory, - sourcePath = path, - sourceHolder = fileSystemPanel, - targetHolder = target, - ) - } - }) - - - rightFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener { - override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) { - val target = leftFileSystemTabbed.getSelectedFileSystemPanel() ?: return - transport( - fileSystemPanel.workdir, target.workdir, - isSourceDirectory = isDirectory, - sourcePath = path, - sourceHolder = fileSystemPanel, - targetHolder = target, - ) - } - }) } fun transport( diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 6a1296d..618cfb0 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -259,6 +259,7 @@ termora.transport.table.owner=Owner # contextmenu termora.transport.table.contextmenu.transfer=Transfer +termora.transport.table.contextmenu.edit=${termora.keymgr.edit} termora.transport.table.contextmenu.copy-path=Copy Path termora.transport.table.contextmenu.open-in-folder=Open in {0} termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}