mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: vfs2
This commit is contained in:
@@ -22,6 +22,10 @@ commons-compress
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-vfs2
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-io
|
commons-io
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ dependencies {
|
|||||||
implementation(libs.commons.net)
|
implementation(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
implementation(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
implementation(libs.commons.compress)
|
||||||
|
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ application {
|
|||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
"-XX:+ZGenerational",
|
"-XX:+ZGenerational",
|
||||||
"-XX:ZUncommitDelay=60",
|
"-XX:ZUncommitDelay=60",
|
||||||
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ commons-csv = "1.14.0"
|
|||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.13.0"
|
commons-text = "1.13.0"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
|
commons-vfs2="2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
jgoodies-forms = "1.9.0"
|
jgoodies-forms = "1.9.0"
|
||||||
jfa = "1.2.0"
|
jfa = "1.2.0"
|
||||||
@@ -54,6 +55,7 @@ commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.
|
|||||||
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||||
|
commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.ref = "commons-vfs2" }
|
||||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||||
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.ActionManager
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileProvider
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatSystemProperties
|
import com.formdev.flatlaf.FlatSystemProperties
|
||||||
import com.formdev.flatlaf.extras.FlatDesktop
|
import com.formdev.flatlaf.extras.FlatDesktop
|
||||||
@@ -17,6 +18,10 @@ import kotlinx.coroutines.launch
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
|
import org.apache.commons.vfs2.cache.WeakRefFilesCache
|
||||||
|
import org.apache.commons.vfs2.impl.DefaultFileSystemManager
|
||||||
|
import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.MenuItem
|
import java.awt.MenuItem
|
||||||
@@ -48,10 +53,17 @@ class ApplicationRunner {
|
|||||||
// 统计
|
// 统计
|
||||||
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
||||||
|
|
||||||
// init ActionManager、KeymapManager
|
// init ActionManager、KeymapManager、VFS
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
ActionManager.getInstance()
|
ActionManager.getInstance()
|
||||||
KeymapManager.getInstance()
|
KeymapManager.getInstance()
|
||||||
|
|
||||||
|
val fileSystemManager = DefaultFileSystemManager()
|
||||||
|
fileSystemManager.addProvider("sftp", MySftpFileProvider())
|
||||||
|
fileSystemManager.addProvider("file", DefaultLocalFileProvider())
|
||||||
|
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||||
|
fileSystemManager.init()
|
||||||
|
VFS.setManager(fileSystemManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 LAF
|
// 设置 LAF
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import app.termora.Icons
|
|||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -14,8 +19,7 @@ import java.awt.event.ActionEvent
|
|||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.awt.event.ItemListener
|
import java.awt.event.ItemListener
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Path
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.PopupMenuEvent
|
import javax.swing.event.PopupMenuEvent
|
||||||
import javax.swing.event.PopupMenuListener
|
import javax.swing.event.PopupMenuListener
|
||||||
@@ -23,8 +27,8 @@ import javax.swing.filechooser.FileSystemView
|
|||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
class FileSystemViewNav(
|
class FileSystemViewNav(
|
||||||
private val fileSystem: FileSystem,
|
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||||
private val homeDirectory: Path
|
private val homeDirectory: FileObject
|
||||||
) : JPanel(BorderLayout()) {
|
) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -38,7 +42,7 @@ class FileSystemViewNav(
|
|||||||
private val history = linkedSetOf<String>()
|
private val history = linkedSetOf<String>()
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val downBtn = JButton(Icons.chevronDown)
|
private val downBtn = JButton(Icons.chevronDown)
|
||||||
private val comboBox = object : JComboBox<Path>() {
|
private val comboBox = object : JComboBox<FileObject>() {
|
||||||
override fun getLocationOnScreen(): Point {
|
override fun getLocationOnScreen(): Point {
|
||||||
val point = super.getLocationOnScreen()
|
val point = super.getLocationOnScreen()
|
||||||
point.y -= 1
|
point.y -= 1
|
||||||
@@ -80,7 +84,7 @@ class FileSystemViewNav(
|
|||||||
): Component {
|
): Component {
|
||||||
val c = super.getListCellRendererComponent(
|
val c = super.getListCellRendererComponent(
|
||||||
list,
|
list,
|
||||||
value,
|
if (value is FileObject) formatDisplayPath(value) else value.toString(),
|
||||||
index,
|
index,
|
||||||
isSelected,
|
isSelected,
|
||||||
cellHasFocus
|
cellHasFocus
|
||||||
@@ -99,12 +103,12 @@ class FileSystemViewNav(
|
|||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
if (fileSystem.isWindows()) {
|
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
try {
|
try {
|
||||||
for (root in fileSystemView.roots) {
|
for (root in fileSystemView.roots) {
|
||||||
history.add(root.absolutePath)
|
history.add(root.absolutePath)
|
||||||
}
|
}
|
||||||
for (rootDirectory in fileSystem.rootDirectories) {
|
for (rootDirectory in FileSystems.getDefault().rootDirectories) {
|
||||||
history.add(rootDirectory.absolutePathString())
|
history.add(rootDirectory.absolutePathString())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -115,12 +119,16 @@ class FileSystemViewNav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatDisplayPath(file: FileObject): String {
|
||||||
|
return file.absolutePathString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
|
||||||
val itemListener = ItemListener { e ->
|
val itemListener = ItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
val item = comboBox.selectedItem
|
val item = comboBox.selectedItem
|
||||||
if (item is Path) {
|
if (item is FileObject) {
|
||||||
changeSelectedPath(item)
|
changeSelectedPath(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +175,11 @@ class FileSystemViewNav(
|
|||||||
val name = textField.text.trim()
|
val name = textField.text.trim()
|
||||||
if (name.isBlank()) return
|
if (name.isBlank()) return
|
||||||
try {
|
try {
|
||||||
changeSelectedPath(fileSystem.getPath(name))
|
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
changeSelectedPath(fileSystem.resolveFile("file://${name}"))
|
||||||
|
} else {
|
||||||
|
changeSelectedPath(fileSystem.resolveFile(name))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -182,7 +194,11 @@ class FileSystemViewNav(
|
|||||||
comboBox.removeAllItems()
|
comboBox.removeAllItems()
|
||||||
|
|
||||||
for (text in history) {
|
for (text in history) {
|
||||||
val path = fileSystem.getPath(text)
|
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
|
VFS.getManager().resolveFile("file://${text}")
|
||||||
|
} else {
|
||||||
|
fileSystem.resolveFile(text)
|
||||||
|
}
|
||||||
comboBox.addItem(path)
|
comboBox.addItem(path)
|
||||||
if (text == textField.text) {
|
if (text == textField.text) {
|
||||||
comboBox.selectedItem = path
|
comboBox.selectedItem = path
|
||||||
@@ -218,15 +234,15 @@ class FileSystemViewNav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectedPath(): Path {
|
fun getSelectedPath(): FileObject {
|
||||||
return textField.getClientProperty(PATH) as Path
|
return textField.getClientProperty(PATH) as FileObject
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeSelectedPath(path: Path) {
|
fun changeSelectedPath(file: FileObject) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
textField.text = path.absolutePathString()
|
textField.text = formatDisplayPath(file)
|
||||||
textField.putClientProperty(PATH, path)
|
textField.putClientProperty(PATH, file)
|
||||||
|
|
||||||
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
||||||
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
|
|||||||
@@ -3,30 +3,28 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.name
|
|
||||||
|
|
||||||
class FileSystemViewPanel(
|
class FileSystemViewPanel(
|
||||||
val host: Host,
|
val host: Host,
|
||||||
val fileSystem: FileSystem,
|
val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
private val coroutineScope: CoroutineScope,
|
||||||
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
||||||
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
@@ -100,7 +98,7 @@ class FileSystemViewPanel(
|
|||||||
override fun onTransportChanged(transport: Transport) {
|
override fun onTransportChanged(transport: Transport) {
|
||||||
val path = transport.target.parent ?: return
|
val path = transport.target.parent ?: return
|
||||||
if (path.fileSystem != fileSystem) return
|
if (path.fileSystem != fileSystem) return
|
||||||
if (path.absolutePathString() != workdir.absolutePathString()) return
|
if (path.name.path != workdir.name.path) return
|
||||||
// 立即刷新
|
// 立即刷新
|
||||||
reload(true)
|
reload(true)
|
||||||
}
|
}
|
||||||
@@ -123,19 +121,19 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
private fun enterTableSelectionFolder(row: Int = table.selectedRow) {
|
private fun enterTableSelectionFolder(row: Int = table.selectedRow) {
|
||||||
if (row < 0 || isLoading.get()) return
|
if (row < 0 || isLoading.get()) return
|
||||||
val attr = model.getAttr(row)
|
val file = model.getFileObject(row)
|
||||||
if (attr.isFile) return
|
if (file.isFile) return
|
||||||
|
|
||||||
// 当前工作目录
|
// 当前工作目录
|
||||||
val workdir = getWorkdir()
|
val workdir = getWorkdir()
|
||||||
|
|
||||||
// 返回上级之后,选中上级目录
|
// 返回上级之后,选中上级目录
|
||||||
if (attr.name == "..") {
|
if (row == 0 && model.hasParent) {
|
||||||
val workdirName = workdir.name
|
val workdirName = workdir.name
|
||||||
nextReloadTickSelection(workdirName)
|
nextReloadTickSelection(workdirName.baseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeWorkdir(attr.path)
|
changeWorkdir(file)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +167,13 @@ class FileSystemViewPanel(
|
|||||||
bookmarkBtn.addActionListener { e ->
|
bookmarkBtn.addActionListener { e ->
|
||||||
if (e.actionCommand.isNullOrBlank()) {
|
if (e.actionCommand.isNullOrBlank()) {
|
||||||
if (bookmarkBtn.isBookmark) {
|
if (bookmarkBtn.isBookmark) {
|
||||||
bookmarkBtn.deleteBookmark(workdir.toString())
|
bookmarkBtn.deleteBookmark(workdir.absolutePathString())
|
||||||
} else {
|
} else {
|
||||||
bookmarkBtn.addBookmark(workdir.toString())
|
bookmarkBtn.addBookmark(workdir.absolutePathString())
|
||||||
}
|
}
|
||||||
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
||||||
} else {
|
} else {
|
||||||
changeWorkdir(fileSystem.getPath(e.actionCommand))
|
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,14 +192,13 @@ class FileSystemViewPanel(
|
|||||||
button.addActionListener(object : AbstractAction() {
|
button.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (model.rowCount < 1) return
|
if (model.rowCount < 1) return
|
||||||
val attr = model.getAttr(0)
|
if (model.hasParent) return
|
||||||
if (attr !is FileSystemViewTableModel.ParentAttr) return
|
|
||||||
enterTableSelectionFolder(0)
|
enterTableSelectionFolder(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addPropertyChangeListener("workdir") {
|
addPropertyChangeListener("workdir") {
|
||||||
button.isEnabled = model.rowCount > 0 && model.getAttr(0) is FileSystemViewTableModel.ParentAttr
|
button.isEnabled = model.rowCount > 0 && model.hasParent
|
||||||
}
|
}
|
||||||
|
|
||||||
return button
|
return button
|
||||||
@@ -211,7 +208,7 @@ class FileSystemViewPanel(
|
|||||||
// 创建成功之后需要修改和选中
|
// 创建成功之后需要修改和选中
|
||||||
registerNextReloadTick {
|
registerNextReloadTick {
|
||||||
for (i in 0 until table.rowCount) {
|
for (i in 0 until table.rowCount) {
|
||||||
if (model.getAttr(i).name == name) {
|
if (model.getFileObject(i).name.baseName == name) {
|
||||||
table.addRowSelectionInterval(i, i)
|
table.addRowSelectionInterval(i, i)
|
||||||
table.scrollRectToVisible(table.getCellRect(i, 0, true))
|
table.scrollRectToVisible(table.getCellRect(i, 0, true))
|
||||||
consumer.accept(i)
|
consumer.accept(i)
|
||||||
@@ -221,18 +218,19 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeWorkdir(workdir: Path) {
|
private fun changeWorkdir(workdir: FileObject) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
nav.changeSelectedPath(workdir)
|
nav.changeSelectedPath(workdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renameTo(oldPath: Path, newPath: Path) {
|
fun renameTo(oldPath: FileObject, newPath: FileObject) {
|
||||||
|
|
||||||
// 新建文件夹
|
// 新建文件夹
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
||||||
if (requestLoading()) {
|
if (requestLoading()) {
|
||||||
try {
|
try {
|
||||||
Files.move(oldPath, newPath, StandardCopyOption.ATOMIC_MOVE)
|
oldPath.moveTo(newPath)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
@@ -247,7 +245,7 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建成功之后需要选中
|
// 创建成功之后需要选中
|
||||||
nextReloadTickSelection(newPath.name)
|
nextReloadTickSelection(newPath.name.baseName)
|
||||||
|
|
||||||
// 立即刷新
|
// 立即刷新
|
||||||
reload()
|
reload()
|
||||||
@@ -258,7 +256,7 @@ class FileSystemViewPanel(
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (requestLoading()) {
|
if (requestLoading()) {
|
||||||
try {
|
try {
|
||||||
doNewFolderOrFile(getWorkdir().resolve(name), isFile)
|
doNewFolderOrFile(getWorkdir().resolveFile(name), isFile)
|
||||||
} finally {
|
} finally {
|
||||||
stopLoading()
|
stopLoading()
|
||||||
}
|
}
|
||||||
@@ -273,9 +271,9 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun doNewFolderOrFile(path: Path, isFile: Boolean) {
|
private suspend fun doNewFolderOrFile(path: FileObject, isFile: Boolean) {
|
||||||
|
|
||||||
if (Files.exists(path)) {
|
if (path.exists()) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
@@ -288,7 +286,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
// 创建文件夹
|
// 创建文件夹
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching { if (isFile) Files.createFile(path) else Files.createDirectories(path) }.onFailure {
|
runCatching { if (isFile) path.createFile() else path.createFolder() }.onFailure {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
if (it is Exception) {
|
if (it is Exception) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
@@ -329,7 +327,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
fun reload(rememberSelection: Boolean = false) {
|
fun reload(rememberSelection: Boolean = false) {
|
||||||
if (!requestLoading()) return
|
if (!requestLoading()) return
|
||||||
if (fileSystem.isSFTP()) loadingPanel.start()
|
if (fileSystem is MySftpFileSystem) loadingPanel.start()
|
||||||
val oldWorkdir = workdir
|
val oldWorkdir = workdir
|
||||||
val path = nav.getSelectedPath()
|
val path = nav.getSelectedPath()
|
||||||
|
|
||||||
@@ -338,7 +336,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
if (rememberSelection) {
|
if (rememberSelection) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
table.selectedRows.sortedDescending().map { model.getAttr(it).name }
|
table.selectedRows.sortedDescending().map { model.getFileObject(it).name.baseName }
|
||||||
.forEach { nextReloadTickSelection(it) }
|
.forEach { nextReloadTickSelection(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,7 +345,7 @@ class FileSystemViewPanel(
|
|||||||
if (it is Exception) {
|
if (it is Exception) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner, ExceptionUtils.getMessage(it),
|
owner, ExceptionUtils.getRootCauseMessage(it),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -367,34 +365,35 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
stopLoading()
|
stopLoading()
|
||||||
if (fileSystem.isSFTP()) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
withContext(Dispatchers.Swing) { loadingPanel.stop() }
|
withContext(Dispatchers.Swing) { loadingPanel.stop() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHomeDirectory(): Path {
|
private fun getHomeDirectory(): FileObject {
|
||||||
if (fileSystem.isSFTP()) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
val fs = fileSystem as SftpFileSystem
|
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
||||||
val host = fs.session.getAttribute(SshClients.HOST_KEY) ?: return fs.defaultDir
|
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
val defaultDirectory = host.options.sftpDefaultDirectory
|
val defaultDirectory = host.options.sftpDefaultDirectory
|
||||||
if (defaultDirectory.isNotBlank()) {
|
if (defaultDirectory.isNotBlank()) {
|
||||||
return runCatching { fs.getPath(defaultDirectory) }
|
return fileSystem.resolveFile(defaultDirectory)
|
||||||
.getOrElse { fs.defaultDir }
|
|
||||||
}
|
}
|
||||||
return fs.defaultDir
|
return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sftp.defaultDirectory.isNotBlank()) {
|
if (sftp.defaultDirectory.isNotBlank()) {
|
||||||
return runCatching { fileSystem.getPath(sftp.defaultDirectory) }
|
val resolveFile = fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
||||||
.getOrElse { fileSystem.getPath(SystemUtils.USER_HOME) }
|
if (resolveFile.exists()) {
|
||||||
|
return resolveFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileSystem.getPath(SystemUtils.USER_HOME)
|
return fileSystem.resolveFile("file://${SystemUtils.USER_HOME}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWorkdir(): Path {
|
fun getWorkdir(): FileObject {
|
||||||
return workdir
|
return workdir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.SettingsAction
|
import app.termora.actions.SettingsAction
|
||||||
|
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
@@ -11,14 +14,11 @@ import com.jgoodies.forms.builder.FormBuilder
|
|||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.apache.sshd.sftp.client.SftpClient
|
import org.apache.commons.vfs2.VFS
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPosixFileAttributes
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -32,8 +32,12 @@ import java.awt.event.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.*
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.FileVisitor
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
import java.text.MessageFormat
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -41,14 +45,14 @@ import java.util.regex.Pattern
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import kotlin.collections.ArrayDeque
|
import kotlin.collections.ArrayDeque
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode", "CascadeIf")
|
||||||
class FileSystemViewTable(
|
class FileSystemViewTable(
|
||||||
private val fileSystem: FileSystem,
|
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope
|
private val coroutineScope: CoroutineScope
|
||||||
) : JTable(), Disposable {
|
) : JTable(), Disposable {
|
||||||
@@ -105,8 +109,8 @@ class FileSystemViewTable(
|
|||||||
): Component {
|
): Component {
|
||||||
foreground = null
|
foreground = null
|
||||||
val c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
val c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
||||||
icon = if (column == FileSystemViewTableModel.COLUMN_NAME) model.getAttr(row).icon else null
|
icon = if (column == FileSystemViewTableModel.COLUMN_NAME) model.getFileIcon(row) else null
|
||||||
foreground = if (!isSelected && model.getAttr(row).isHidden)
|
foreground = if (!isSelected && model.getFileObject(row).isHidden)
|
||||||
UIManager.getColor("textInactiveText") else foreground
|
UIManager.getColor("textInactiveText") else foreground
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -141,10 +145,10 @@ class FileSystemViewTable(
|
|||||||
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
val row = table.selectedRow
|
val row = table.selectedRow
|
||||||
if (row <= 0 || row >= table.rowCount) return
|
if (row <= 0 || row >= table.rowCount) return
|
||||||
val attr = model.getAttr(row)
|
val file = model.getFileObject(row)
|
||||||
if (attr.isDirectory) return
|
if (file.isFolder) return
|
||||||
// 传输
|
// 传输
|
||||||
transfer(arrayOf(attr))
|
transfer(listOf(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -156,8 +160,7 @@ class FileSystemViewTable(
|
|||||||
if ((SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_BACK_SPACE) || (e.keyCode == KeyEvent.VK_DELETE)) {
|
if ((SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_BACK_SPACE) || (e.keyCode == KeyEvent.VK_DELETE)) {
|
||||||
val rows = selectedRows
|
val rows = selectedRows
|
||||||
if (rows.contains(0)) return
|
if (rows.contains(0)) return
|
||||||
val attrs = rows.map { model.getAttr(it) }.toTypedArray()
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val files = attrs.map { it.path }.toTypedArray()
|
|
||||||
deletePaths(files, false)
|
deletePaths(files, false)
|
||||||
} else if (!SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_F5) {
|
} else if (!SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_F5) {
|
||||||
fileSystemViewPanel.reload(true)
|
fileSystemViewPanel.reload(true)
|
||||||
@@ -173,13 +176,15 @@ class FileSystemViewTable(
|
|||||||
// 如果不是新增行,并且光标不在第一列,那么不允许
|
// 如果不是新增行,并且光标不在第一列,那么不允许
|
||||||
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
||||||
// 如果不是新增行,如果在一个文件上,那么不允许
|
// 如果不是新增行,如果在一个文件上,那么不允许
|
||||||
if (!dropLocation.isInsertRow && model.getAttr(dropLocation.row).isFile) return false
|
if (!dropLocation.isInsertRow && model.getFileObject(dropLocation.row).isFile) return false
|
||||||
|
// 如果不是新增行,在 .. 上面,不允许
|
||||||
|
if (!dropLocation.isInsertRow && model.hasParent && dropLocation.row == 0) return false
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
return data is FileSystemTableRowTransferable && data.source != table
|
return data is FileSystemTableRowTransferable && data.source != table
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
return !fileSystem.isLocal()
|
return fileSystem !is LocalFileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -190,27 +195,25 @@ class FileSystemViewTable(
|
|||||||
// 如果不是新增行,并且光标不在第一列,那么不允许
|
// 如果不是新增行,并且光标不在第一列,那么不允许
|
||||||
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
||||||
// 如果不是新增行,如果在一个文件上,那么不允许
|
// 如果不是新增行,如果在一个文件上,那么不允许
|
||||||
if (!dropLocation.isInsertRow && model.getAttr(dropLocation.row).isFile) return false
|
if (!dropLocation.isInsertRow && model.getFileObject(dropLocation.row).isFile) return false
|
||||||
|
|
||||||
var targetWorkdir: Path? = null
|
var targetWorkdir: FileObject? = null
|
||||||
|
|
||||||
// 变更工作目录
|
// 变更工作目录
|
||||||
if (!dropLocation.isInsertRow) {
|
if (!dropLocation.isInsertRow) {
|
||||||
targetWorkdir = model.getAttr(dropLocation.row).path
|
targetWorkdir = model.getFileObject(dropLocation.row)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
if (data !is FileSystemTableRowTransferable) return false
|
if (data !is FileSystemTableRowTransferable) return false
|
||||||
// 委托源表开始传输
|
// 委托源表开始传输
|
||||||
data.source.transfer(data.attrs.toTypedArray(), false, targetWorkdir)
|
data.source.transfer(data.files, false, targetWorkdir)
|
||||||
return true
|
return true
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
if (files.isEmpty()) return false
|
if (files.isEmpty()) return false
|
||||||
val paths = files.filterIsInstance<File>()
|
val paths = files.filterIsInstance<File>().map { VFS.getManager().resolveFile(it.toURI()) }
|
||||||
.map { FileSystemViewTableModel.Attr(it.toPath()) }
|
|
||||||
.toTypedArray()
|
|
||||||
if (paths.isEmpty()) return false
|
if (paths.isEmpty()) return false
|
||||||
val localTarget = sftpPanel.getLocalTarget()
|
val localTarget = sftpPanel.getLocalTarget()
|
||||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||||
@@ -226,9 +229,9 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createTransferable(c: JComponent?): Transferable? {
|
override fun createTransferable(c: JComponent?): Transferable? {
|
||||||
val attrs = table.selectedRows.filter { it != 0 }.map { model.getAttr(it) }
|
val files = table.selectedRows.filter { it != 0 }.map { model.getFileObject(it) }
|
||||||
if (attrs.isEmpty()) return null
|
if (files.isEmpty()) return null
|
||||||
return FileSystemTableRowTransferable(table, attrs)
|
return FileSystemTableRowTransferable(table, files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +246,7 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigate(row: Int, c: Char): Boolean {
|
private fun navigate(row: Int, c: Char): Boolean {
|
||||||
val name = model.getAttr(row).name
|
val name = model.getFileObject(row).name.baseName
|
||||||
if (name.startsWith(c, true)) {
|
if (name.startsWith(c, true)) {
|
||||||
clearSelection()
|
clearSelection()
|
||||||
addRowSelectionInterval(row, row)
|
addRowSelectionInterval(row, row)
|
||||||
@@ -255,18 +258,8 @@ class FileSystemViewTable(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
if (isDisposed.compareAndSet(false, true)) {
|
|
||||||
if (!fileSystem.isSFTP()) {
|
|
||||||
coroutineScope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
||||||
val attrs = rows.map { model.getAttr(it) }.toTypedArray()
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val files = attrs.map { it.path }.toTypedArray()
|
|
||||||
val hasParent = rows.contains(0)
|
val hasParent = rows.contains(0)
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
@@ -279,13 +272,13 @@ class FileSystemViewTable(
|
|||||||
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
||||||
// 编辑
|
// 编辑
|
||||||
val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit"))
|
val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit"))
|
||||||
edit.isEnabled = fileSystem.isSFTP() && attrs.all { it.isFile }
|
edit.isEnabled = fileSystem is MySftpFileSystem && files.all { it.isFile }
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
// 复制路径
|
// 复制路径
|
||||||
val copyPath = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
val copyPath = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
||||||
|
|
||||||
// 如果是本地,那么支持打开本地路径
|
// 如果是本地,那么支持打开本地路径
|
||||||
if (fileSystem.isLocal()) {
|
if (fileSystem is LocalFileSystem) {
|
||||||
popupMenu.add(
|
popupMenu.add(
|
||||||
I18n.getString(
|
I18n.getString(
|
||||||
"termora.transport.table.contextmenu.open-in-folder",
|
"termora.transport.table.contextmenu.open-in-folder",
|
||||||
@@ -294,7 +287,7 @@ class FileSystemViewTable(
|
|||||||
else I18n.getString("termora.folder")
|
else I18n.getString("termora.folder")
|
||||||
)
|
)
|
||||||
).addActionListener {
|
).addActionListener {
|
||||||
Application.browseInFolder(files.last().toFile())
|
Application.browseInFolder(File(files.last().absolutePathString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -307,18 +300,15 @@ class FileSystemViewTable(
|
|||||||
val delete = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.delete"))
|
val delete = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||||
// rm -rf
|
// rm -rf
|
||||||
val rmrf = popupMenu.add(JMenuItem("rm -rf", Icons.warningIntroduction))
|
val rmrf = popupMenu.add(JMenuItem("rm -rf", Icons.warningIntroduction))
|
||||||
|
|
||||||
// 只有 SFTP 可以
|
// 只有 SFTP 可以
|
||||||
if (!fileSystem.isSFTP()) {
|
rmrf.isVisible = fileSystem is MySftpFileSystem
|
||||||
rmrf.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改权限
|
// 修改权限
|
||||||
val permission = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
|
val permission = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
|
||||||
permission.isEnabled = false
|
permission.isEnabled = false
|
||||||
|
|
||||||
// 如果是本地系统文件,那么不允许修改权限,用户应该自己修改
|
// 如果是本地系统文件,那么不允许修改权限,用户应该自己修改
|
||||||
if (fileSystem.isSFTP() && rows.isNotEmpty()) {
|
if (fileSystem is MySftpFileSystem && rows.isNotEmpty()) {
|
||||||
permission.isEnabled = true
|
permission.isEnabled = true
|
||||||
}
|
}
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
@@ -360,23 +350,25 @@ class FileSystemViewTable(
|
|||||||
})
|
})
|
||||||
copyPath.addActionListener {
|
copyPath.addActionListener {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
attrs.forEach { sb.append(it.path.absolutePathString()).appendLine() }
|
files.forEach { sb.append(it.absolutePathString()).appendLine() }
|
||||||
sb.deleteCharAt(sb.length - 1)
|
sb.deleteCharAt(sb.length - 1)
|
||||||
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
||||||
}
|
}
|
||||||
edit.addActionListener { if (files.isNotEmpty()) editFiles(files) }
|
edit.addActionListener { if (files.isNotEmpty()) editFiles(files) }
|
||||||
permission.addActionListener(object : AbstractAction() {
|
permission.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val last = attrs.last()
|
val last = files.last()
|
||||||
|
if (last !is MySftpFileObject) return
|
||||||
|
|
||||||
val dialog = PosixFilePermissionDialog(
|
val dialog = PosixFilePermissionDialog(
|
||||||
SwingUtilities.getWindowAncestor(table),
|
SwingUtilities.getWindowAncestor(table),
|
||||||
last.posixFilePermissions
|
model.getFilePermissions(last)
|
||||||
)
|
)
|
||||||
val permissions = dialog.open() ?: return
|
val permissions = dialog.open() ?: return
|
||||||
|
|
||||||
if (fileSystemViewPanel.requestLoading()) {
|
if (fileSystemViewPanel.requestLoading()) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
val c = runCatching { Files.setPosixFilePermissions(last.path, permissions) }.onFailure {
|
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
@@ -398,7 +390,7 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||||
transfer.addActionListener { transfer(attrs) }
|
transfer.addActionListener { transfer(files) }
|
||||||
|
|
||||||
if (rows.isEmpty() || hasParent) {
|
if (rows.isEmpty() || hasParent) {
|
||||||
transfer.isEnabled = false
|
transfer.isEnabled = false
|
||||||
@@ -419,13 +411,13 @@ class FileSystemViewTable(
|
|||||||
private fun renameSelection() {
|
private fun renameSelection() {
|
||||||
val index = selectedRow
|
val index = selectedRow
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
val attr = model.getAttr(index)
|
val file = model.getFileObject(index)
|
||||||
val text = OptionPane.showInputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
owner,
|
owner,
|
||||||
value = attr.name,
|
value = file.name.baseName,
|
||||||
title = I18n.getString("termora.transport.table.contextmenu.rename")
|
title = I18n.getString("termora.transport.table.contextmenu.rename")
|
||||||
) ?: return
|
) ?: return
|
||||||
if (text.isBlank() || text == attr.name) return
|
if (text.isBlank() || text == file.name.baseName) return
|
||||||
if (model.getPathNames().contains(text)) {
|
if (model.getPathNames().contains(text)) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
@@ -434,10 +426,11 @@ class FileSystemViewTable(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileSystemViewPanel.renameTo(attr.path, attr.path.parent.resolve(text))
|
|
||||||
|
fileSystemViewPanel.renameTo(file, file.parent.resolveFile(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun editFiles(files: Array<Path>) {
|
private fun editFiles(files: List<FileObject>) {
|
||||||
if (files.isEmpty()) return
|
if (files.isEmpty()) return
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
@@ -455,10 +448,11 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
val dir = Application.createSubTemporaryDir()
|
val dir = Application.createSubTemporaryDir()
|
||||||
val path = Paths.get(dir.absolutePathString(), file.name)
|
val path = Paths.get(dir.absolutePathString(), file.name.baseName)
|
||||||
|
val target = VFS.getManager().resolveFile("file://" + path.absolutePathString())
|
||||||
|
|
||||||
val newTransport = createTransport(file, false, 0L)
|
val newTransport = createTransport(file, false, 0L)
|
||||||
.apply { target = path }
|
.apply { this.target = target }
|
||||||
|
|
||||||
transportManager.addTransportListener(object : TransportListener {
|
transportManager.addTransportListener(object : TransportListener {
|
||||||
override fun onTransportChanged(transport: Transport) {
|
override fun onTransportChanged(transport: Transport) {
|
||||||
@@ -467,7 +461,7 @@ class FileSystemViewTable(
|
|||||||
transportManager.removeTransportListener(this)
|
transportManager.removeTransportListener(this)
|
||||||
if (transport.status != TransportStatus.Done) return
|
if (transport.status != TransportStatus.Done) return
|
||||||
// 监听文件变动
|
// 监听文件变动
|
||||||
listenFileChange(path, file)
|
listenFileChange(target, file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -476,21 +470,15 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenFileChange(localPath: Path, remotePath: Path) {
|
private fun listenFileChange(localPath: FileObject, remotePath: FileObject) {
|
||||||
try {
|
try {
|
||||||
|
val p = localPath.absolutePathString()
|
||||||
if (sftp.editCommand.isNotBlank()) {
|
if (sftp.editCommand.isNotBlank()) {
|
||||||
ProcessBuilder(
|
ProcessBuilder(parseCommand(MessageFormat.format(sftp.editCommand, p))).start()
|
||||||
parseCommand(
|
|
||||||
MessageFormat.format(
|
|
||||||
sftp.editCommand,
|
|
||||||
localPath.absolutePathString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).start()
|
|
||||||
} else if (SystemInfo.isMacOS) {
|
} else if (SystemInfo.isMacOS) {
|
||||||
ProcessBuilder("open", "-a", "TextEdit", localPath.absolutePathString()).start()
|
ProcessBuilder("open", "-a", "TextEdit", p).start()
|
||||||
} else if (SystemInfo.isWindows) {
|
} else if (SystemInfo.isWindows) {
|
||||||
ProcessBuilder("notepad", localPath.absolutePathString()).start()
|
ProcessBuilder("notepad", p).start()
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -501,13 +489,17 @@ class FileSystemViewTable(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastModifiedTime = localPath.getLastModifiedTime().toMillis()
|
var lastModifiedTime = localPath.content.lastModifiedTime
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
while (coroutineScope.isActive) {
|
while (coroutineScope.isActive) {
|
||||||
try {
|
try {
|
||||||
if (isDisposed.get() || !Files.exists(localPath)) break
|
|
||||||
val nowModifiedTime = localPath.getLastModifiedTime().toMillis()
|
if (isDisposed.get()) break
|
||||||
|
localPath.refresh()
|
||||||
|
if (!localPath.exists()) break
|
||||||
|
|
||||||
|
val nowModifiedTime = localPath.content.lastModifiedTime
|
||||||
if (nowModifiedTime != lastModifiedTime) {
|
if (nowModifiedTime != lastModifiedTime) {
|
||||||
lastModifiedTime = nowModifiedTime
|
lastModifiedTime = nowModifiedTime
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
@@ -562,7 +554,7 @@ class FileSystemViewTable(
|
|||||||
fileSystemViewPanel.newFolderOrFile(text, isFile)
|
fileSystemViewPanel.newFolderOrFile(text, isFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deletePaths(paths: Array<Path>, rm: Boolean = false) {
|
private fun deletePaths(paths: List<FileObject>, rm: Boolean = false) {
|
||||||
if (OptionPane.showConfirmDialog(
|
if (OptionPane.showConfirmDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
I18n.getString(if (rm) "termora.transport.table.contextmenu.rm-warning" else "termora.transport.table.contextmenu.delete-warning"),
|
I18n.getString(if (rm) "termora.transport.table.contextmenu.rm-warning" else "termora.transport.table.contextmenu.delete-warning"),
|
||||||
@@ -576,10 +568,10 @@ class FileSystemViewTable(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (fileSystem.isSFTP()) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
deleteSftpPaths(paths, rm)
|
deleteSftpPaths(paths, rm)
|
||||||
} else {
|
} else {
|
||||||
deleteRecursively(paths)
|
deleteRecursively(paths)
|
||||||
@@ -590,97 +582,74 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止加载
|
withContext(Dispatchers.Swing) {
|
||||||
fileSystemViewPanel.stopLoading()
|
// 停止加载
|
||||||
|
fileSystemViewPanel.stopLoading()
|
||||||
// 刷新
|
// 刷新
|
||||||
fileSystemViewPanel.reload()
|
fileSystemViewPanel.reload()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteSftpPaths(paths: Array<Path>, rm: Boolean = false) {
|
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
||||||
val fs = this.fileSystem as SftpFileSystem
|
|
||||||
if (rm) {
|
if (rm) {
|
||||||
for (path in paths) {
|
val session = (this.fileSystem as MySftpFileSystem).getClientSession()
|
||||||
fs.session.executeRemoteCommand(
|
for (path in files) {
|
||||||
|
session.executeRemoteCommand(
|
||||||
"rm -rf '${path.absolutePathString()}'",
|
"rm -rf '${path.absolutePathString()}'",
|
||||||
OutputStream.nullOutputStream(),
|
OutputStream.nullOutputStream(),
|
||||||
Charsets.UTF_8
|
Charsets.UTF_8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.client.use {
|
deleteRecursively(files)
|
||||||
for (path in paths) {
|
|
||||||
deleteRecursivelySFTP(path as SftpPath, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteRecursively(paths: Array<Path>) {
|
private fun deleteRecursively(files: List<FileObject>) {
|
||||||
for (path in paths) {
|
for (path in files) {
|
||||||
FileUtils.deleteQuietly(path.toFile())
|
path.deleteAll()
|
||||||
|
path.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 优化删除效率,采用一个连接
|
|
||||||
*/
|
|
||||||
private fun deleteRecursivelySFTP(path: SftpPath, sftpClient: SftpClient) {
|
|
||||||
val isDirectory = if (path.attributes != null) path.attributes.isDirectory else path.isDirectory()
|
|
||||||
if (isDirectory) {
|
|
||||||
for (e in sftpClient.readDir(path.toString())) {
|
|
||||||
if (e.filename == ".." || e.filename == ".") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (e.attributes.isDirectory) {
|
|
||||||
deleteRecursivelySFTP(path.resolve(e.filename), sftpClient)
|
|
||||||
} else {
|
|
||||||
sftpClient.remove(path.resolve(e.filename).toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sftpClient.rmdir(path.toString())
|
|
||||||
} else {
|
|
||||||
sftpClient.remove(path.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transfer(
|
private fun transfer(
|
||||||
attrs: Array<FileSystemViewTableModel.Attr>,
|
files: List<FileObject>,
|
||||||
fromLocalSystem: Boolean = false,
|
fromLocalSystem: Boolean = false,
|
||||||
targetWorkdir: Path? = null
|
targetWorkdir: FileObject? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = sftpPanel.getTarget(table) ?: return
|
||||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||||
var overwriteAll = false
|
var isApplyAll = false
|
||||||
|
var lastAction = Action.Overwrite
|
||||||
|
|
||||||
for (attr in attrs) {
|
for (file in files) {
|
||||||
|
if (!isApplyAll && (targetWorkdir == null || target.getWorkdir() == targetWorkdir)) {
|
||||||
if (!overwriteAll) {
|
val targetAttr = 0.rangeUntil(table.model.rowCount).map { table.model.getFileObject(it) }
|
||||||
val targetAttr = 0.rangeUntil(table.model.rowCount).map { table.model.getAttr(it) }
|
.find { it.name.baseName == file.name.baseName }
|
||||||
.find { it.name == attr.name }
|
|
||||||
if (targetAttr != null) {
|
if (targetAttr != null) {
|
||||||
val askTransfer = askTransfer(attr, targetAttr)
|
val askTransfer = askTransfer(file, targetAttr)
|
||||||
if (askTransfer.option != JOptionPane.YES_OPTION) {
|
if (askTransfer.option != JOptionPane.YES_OPTION) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (askTransfer.action == AskTransfer.Action.Skip) {
|
if (askTransfer.action == Action.Skip) {
|
||||||
if (askTransfer.applyAll) break
|
if (askTransfer.applyAll) break
|
||||||
continue
|
continue
|
||||||
} else if (askTransfer.action == AskTransfer.Action.Overwrite) {
|
} else {
|
||||||
overwriteAll = askTransfer.applyAll
|
lastAction = askTransfer.action
|
||||||
|
isApplyAll = askTransfer.applyAll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
doTransfer(attr, fromLocalSystem, targetWorkdir)
|
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -698,13 +667,14 @@ class FileSystemViewTable(
|
|||||||
) {
|
) {
|
||||||
enum class Action {
|
enum class Action {
|
||||||
Overwrite,
|
Overwrite,
|
||||||
|
Append,
|
||||||
Skip
|
Skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun askTransfer(
|
private fun askTransfer(
|
||||||
sourceAttr: FileSystemViewTableModel.Attr,
|
sourceFile: FileObject,
|
||||||
targetAttr: FileSystemViewTableModel.Attr
|
targetFile: FileObject
|
||||||
): AskTransfer {
|
): AskTransfer {
|
||||||
val formMargin = "7dlu"
|
val formMargin = "7dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
@@ -715,34 +685,29 @@ class FileSystemViewTable(
|
|||||||
val iconSize = 36
|
val iconSize = 36
|
||||||
|
|
||||||
val targetIcon = if (SystemInfo.isWindows)
|
val targetIcon = if (SystemInfo.isWindows)
|
||||||
NativeFileIcons.getIcon(targetAttr.name, targetAttr.isFile, iconSize, iconSize).first
|
model.getFileIcon(targetFile, iconSize, iconSize)
|
||||||
else if (targetAttr.isDirectory) {
|
else if (targetFile.isFolder) {
|
||||||
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||||
} else {
|
} else {
|
||||||
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceIcon = if (SystemInfo.isWindows)
|
val sourceIcon = if (SystemInfo.isWindows)
|
||||||
NativeFileIcons.getIcon(sourceAttr.name, sourceAttr.isFile, iconSize, iconSize).first
|
model.getFileIcon(sourceFile, iconSize, iconSize)
|
||||||
else if (sourceAttr.isDirectory) {
|
else if (sourceFile.isFolder) {
|
||||||
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||||
} else {
|
} else {
|
||||||
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceModified = if (sourceAttr.modified > 0) DateFormatUtils.format(
|
|
||||||
Date(sourceAttr.modified),
|
|
||||||
"yyyy/MM/dd HH:mm"
|
|
||||||
) else "-"
|
|
||||||
|
|
||||||
val targetModified = if (targetAttr.modified > 0) DateFormatUtils.format(
|
val sourceModified = StringUtils.defaultIfBlank(model.getLastModifiedTime(sourceFile), "-")
|
||||||
Date(targetAttr.modified),
|
val targetModified = StringUtils.defaultIfBlank(model.getLastModifiedTime(targetFile), "-")
|
||||||
"yyyy/MM/dd HH:mm"
|
|
||||||
) else "-"
|
|
||||||
|
|
||||||
val actionsComBoBox = JComboBox<AskTransfer.Action>()
|
val actionsComBoBox = JComboBox<Action>()
|
||||||
actionsComBoBox.addItem(AskTransfer.Action.Overwrite)
|
actionsComBoBox.addItem(Action.Overwrite)
|
||||||
actionsComBoBox.addItem(AskTransfer.Action.Skip)
|
actionsComBoBox.addItem(Action.Append)
|
||||||
|
actionsComBoBox.addItem(Action.Skip)
|
||||||
actionsComBoBox.renderer = object : DefaultListCellRenderer() {
|
actionsComBoBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
list: JList<*>?,
|
list: JList<*>?,
|
||||||
@@ -752,10 +717,12 @@ class FileSystemViewTable(
|
|||||||
cellHasFocus: Boolean
|
cellHasFocus: Boolean
|
||||||
): Component {
|
): Component {
|
||||||
var text = value?.toString() ?: StringUtils.EMPTY
|
var text = value?.toString() ?: StringUtils.EMPTY
|
||||||
if (value == AskTransfer.Action.Overwrite) {
|
if (value == Action.Overwrite) {
|
||||||
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
||||||
} else if (value == AskTransfer.Action.Skip) {
|
} else if (value == Action.Skip) {
|
||||||
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
||||||
|
} else if (value == Action.Append) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.append")
|
||||||
}
|
}
|
||||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
}
|
}
|
||||||
@@ -781,11 +748,11 @@ class FileSystemViewTable(
|
|||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
// tip
|
// tip
|
||||||
.add(JLabel(warningIcon)).xy(1, rows, "center, fill")
|
.add(JLabel(warningIcon)).xy(1, rows)
|
||||||
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
||||||
// name
|
// name
|
||||||
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
||||||
.add(sourceAttr.name).xyw(3, rows, 3).apply { rows += step }
|
.add(sourceFile.name.baseName).xyw(3, rows, 3).apply { rows += step }
|
||||||
// separator
|
// separator
|
||||||
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||||
// Destination
|
// Destination
|
||||||
@@ -813,12 +780,13 @@ class FileSystemViewTable(
|
|||||||
owner, panel,
|
owner, panel,
|
||||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
title = sourceAttr.name,
|
title = sourceFile.name.baseName,
|
||||||
initialValue = JOptionPane.YES_OPTION,
|
initialValue = JOptionPane.YES_OPTION,
|
||||||
) {
|
) {
|
||||||
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
||||||
|
it.setLocationRelativeTo(it.owner)
|
||||||
},
|
},
|
||||||
action = actionsComBoBox.selectedItem as AskTransfer.Action,
|
action = actionsComBoBox.selectedItem as Action,
|
||||||
applyAll = applyAllCheckbox.isSelected
|
applyAll = applyAllCheckbox.isSelected
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -829,9 +797,10 @@ class FileSystemViewTable(
|
|||||||
* 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止
|
* 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止
|
||||||
*/
|
*/
|
||||||
private fun doTransfer(
|
private fun doTransfer(
|
||||||
attr: FileSystemViewTableModel.Attr,
|
file: FileObject,
|
||||||
|
action: Action,
|
||||||
fromLocalSystem: Boolean,
|
fromLocalSystem: Boolean,
|
||||||
targetWorkdir: Path?
|
targetWorkdir: FileObject?
|
||||||
) {
|
) {
|
||||||
val sftpPanel = this.sftpPanel
|
val sftpPanel = this.sftpPanel
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = sftpPanel.getTarget(table) ?: return
|
||||||
@@ -841,9 +810,14 @@ class FileSystemViewTable(
|
|||||||
*/
|
*/
|
||||||
val adder = object {
|
val adder = object {
|
||||||
fun add(transport: Transport): Boolean {
|
fun add(transport: Transport): Boolean {
|
||||||
|
if (action == Action.Append) {
|
||||||
|
transport.mode = StandardOpenOption.APPEND
|
||||||
|
} else {
|
||||||
|
transport.mode = StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
}
|
||||||
return addTransport(
|
return addTransport(
|
||||||
sftpPanel,
|
sftpPanel,
|
||||||
if (fromLocalSystem) attr.path.parent else null,
|
if (fromLocalSystem) file.parent else null,
|
||||||
target,
|
target,
|
||||||
targetWorkdir,
|
targetWorkdir,
|
||||||
transport
|
transport
|
||||||
@@ -851,8 +825,8 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attr.isFile) {
|
if (file.isFile) {
|
||||||
adder.add(createTransport(attr.path, false, 0).apply { scanned() })
|
adder.add(createTransport(file, false, 0).apply { scanned() })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,26 +834,26 @@ class FileSystemViewTable(
|
|||||||
var isTerminate = false
|
var isTerminate = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
walk(attr.path, object : FileVisitor<Path> {
|
walk(file, object : FileVisitor<FileObject> {
|
||||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
|
override fun preVisitDirectory(dir: FileObject, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
val transport = createTransport(dir, true, queue.lastOrNull()?.id ?: 0L)
|
val transport = createTransport(dir, true, queue.lastOrNull()?.id ?: 0L)
|
||||||
.apply { queue.addLast(this) }
|
.apply { queue.addLast(this) }
|
||||||
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
||||||
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
override fun visitFile(file: FileObject, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
if (queue.isEmpty()) return FileVisitResult.SKIP_SIBLINGS
|
if (queue.isEmpty()) return FileVisitResult.SKIP_SIBLINGS
|
||||||
val transport = createTransport(file, false, queue.last().id).apply { scanned() }
|
val transport = createTransport(file, false, queue.last().id).apply { scanned() }
|
||||||
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
||||||
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
|
override fun visitFileFailed(file: FileObject, exc: IOException): FileVisitResult {
|
||||||
return FileVisitResult.CONTINUE
|
return FileVisitResult.CONTINUE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
override fun postVisitDirectory(dir: FileObject, exc: IOException?): FileVisitResult {
|
||||||
// 标记为扫描完毕
|
// 标记为扫描完毕
|
||||||
queue.removeLast().scanned()
|
queue.removeLast().scanned()
|
||||||
return FileVisitResult.CONTINUE
|
return FileVisitResult.CONTINUE
|
||||||
@@ -890,6 +864,13 @@ class FileSystemViewTable(
|
|||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
}
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
message = ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
isTerminate = true
|
isTerminate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,35 +880,28 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun walk(dir: Path, visitor: FileVisitor<Path>) {
|
|
||||||
if (fileSystem is SftpFileSystem) {
|
|
||||||
val attr = SftpPosixFileAttributes(dir, SftpClient.Attributes())
|
|
||||||
fileSystem.client.use { walkSFTP(dir, attr, visitor, it) }
|
|
||||||
} else {
|
|
||||||
Files.walkFileTree(dir, setOf(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, visitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun walkSFTP(
|
private fun walk(
|
||||||
dir: Path,
|
dir: FileObject,
|
||||||
attr: SftpPosixFileAttributes,
|
visitor: FileVisitor<FileObject>,
|
||||||
visitor: FileVisitor<Path>,
|
|
||||||
client: SftpClient
|
|
||||||
): FileVisitResult {
|
): FileVisitResult {
|
||||||
|
|
||||||
if (visitor.preVisitDirectory(dir, attr) == FileVisitResult.TERMINATE) {
|
// clear cache
|
||||||
|
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||||
return FileVisitResult.TERMINATE
|
return FileVisitResult.TERMINATE
|
||||||
}
|
}
|
||||||
|
|
||||||
val paths = client.readDir(dir.absolutePathString())
|
for (e in dir.children) {
|
||||||
for (e in paths) {
|
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||||
if (e.filename == ".." || e.filename == ".") continue
|
if (e.isFolder) {
|
||||||
if (e.attributes.isDirectory) {
|
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||||
if (walkSFTP(dir.resolve(e.filename), attr, visitor, client) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
return FileVisitResult.TERMINATE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val result = visitor.visitFile(dir.resolve(e.filename), attr)
|
val result = visitor.visitFile(
|
||||||
|
dir.resolveFile(e.name.baseName),
|
||||||
|
EmptyBasicFileAttributes.INSTANCE
|
||||||
|
)
|
||||||
if (result == FileVisitResult.TERMINATE) {
|
if (result == FileVisitResult.TERMINATE) {
|
||||||
return FileVisitResult.TERMINATE
|
return FileVisitResult.TERMINATE
|
||||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||||
@@ -945,19 +919,22 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
private fun addTransport(
|
private fun addTransport(
|
||||||
sftpPanel: SFTPPanel,
|
sftpPanel: SFTPPanel,
|
||||||
sourceWorkdir: Path?,
|
sourceWorkdir: FileObject?,
|
||||||
target: FileSystemViewPanel,
|
target: FileSystemViewPanel,
|
||||||
targetWorkdir: Path?,
|
targetWorkdir: FileObject?,
|
||||||
transport: Transport
|
transport: Transport
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return try {
|
return try {
|
||||||
sftpPanel.addTransport(table, sourceWorkdir, target, targetWorkdir, transport)
|
sftpPanel.addTransport(table, sourceWorkdir, target, targetWorkdir, transport)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTransport(source: Path, isDirectory: Boolean, parentId: Long): Transport {
|
private fun createTransport(source: FileObject, isDirectory: Boolean, parentId: Long): Transport {
|
||||||
val transport = Transport(
|
val transport = Transport(
|
||||||
source = source,
|
source = source,
|
||||||
target = source,
|
target = source,
|
||||||
@@ -965,7 +942,7 @@ class FileSystemViewTable(
|
|||||||
isDirectory = isDirectory,
|
isDirectory = isDirectory,
|
||||||
)
|
)
|
||||||
if (transport.isFile) {
|
if (transport.isFile) {
|
||||||
transport.filesize.addAndGet(source.fileSize())
|
transport.filesize.addAndGet(source.content.size)
|
||||||
}
|
}
|
||||||
return transport
|
return transport
|
||||||
}
|
}
|
||||||
@@ -973,7 +950,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
private class FileSystemTableRowTransferable(
|
private class FileSystemTableRowTransferable(
|
||||||
val source: FileSystemViewTable,
|
val source: FileSystemViewTable,
|
||||||
val attrs: List<FileSystemViewTableModel.Attr>
|
val files: List<FileObject>
|
||||||
) : Transferable {
|
) : Transferable {
|
||||||
companion object {
|
companion object {
|
||||||
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
||||||
@@ -996,4 +973,47 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = EmptyBasicFileAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastModifiedTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastAccessTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun creationTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRegularFile(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDirectory(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSymbolicLink(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOther(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Long {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileKey(): Any {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,21 +3,24 @@ package app.termora.sftp
|
|||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.NativeStringComparator
|
import app.termora.NativeStringComparator
|
||||||
import app.termora.formatBytes
|
import app.termora.formatBytes
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileType
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.nio.file.attribute.PosixFilePermissions
|
import java.nio.file.attribute.PosixFilePermissions
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
import kotlin.io.path.*
|
|
||||||
|
|
||||||
class FileSystemViewTableModel : DefaultTableModel() {
|
class FileSystemViewTableModel : DefaultTableModel() {
|
||||||
|
|
||||||
@@ -29,9 +32,10 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
const val COLUMN_LAST_MODIFIED_TIME = 3
|
const val COLUMN_LAST_MODIFIED_TIME = 3
|
||||||
const val COLUMN_ATTRS = 4
|
const val COLUMN_ATTRS = 4
|
||||||
const val COLUMN_OWNER = 5
|
const val COLUMN_OWNER = 5
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(FileSystemViewTableModel::class.java)
|
private val log = LoggerFactory.getLogger(FileSystemViewTableModel::class.java)
|
||||||
|
|
||||||
private fun fromSftpPermissions(sftpPermissions: Int): Set<PosixFilePermission> {
|
fun fromSftpPermissions(sftpPermissions: Int): Set<PosixFilePermission> {
|
||||||
val result = mutableSetOf<PosixFilePermission>()
|
val result = mutableSetOf<PosixFilePermission>()
|
||||||
|
|
||||||
// 将十进制权限转换为八进制字符串
|
// 将十进制权限转换为八进制字符串
|
||||||
@@ -68,23 +72,69 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValueAt(row: Int, column: Int): Any {
|
var hasParent: Boolean = false
|
||||||
val attr = getAttr(row)
|
private set
|
||||||
return when (column) {
|
|
||||||
COLUMN_NAME -> attr.name
|
|
||||||
COLUMN_FILE_SIZE -> if (attr.isDirectory) StringUtils.EMPTY else formatBytes(attr.size)
|
|
||||||
COLUMN_TYPE -> attr.type
|
|
||||||
COLUMN_LAST_MODIFIED_TIME -> if (attr.modified > 0) DateFormatUtils.format(
|
|
||||||
Date(attr.modified),
|
|
||||||
"yyyy/MM/dd HH:mm"
|
|
||||||
) else StringUtils.EMPTY
|
|
||||||
|
|
||||||
COLUMN_ATTRS -> attr.permissions
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
COLUMN_OWNER -> attr.owner
|
val file = getFileObject(row)
|
||||||
else -> StringUtils.EMPTY
|
val isParentRow = hasParent && row == 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file.type == FileType.IMAGINARY) return StringUtils.EMPTY
|
||||||
|
return when (column) {
|
||||||
|
COLUMN_NAME -> if (isParentRow) ".." else file.name.baseName
|
||||||
|
COLUMN_FILE_SIZE -> if (isParentRow || file.isFolder) StringUtils.EMPTY else formatBytes(file.content.size)
|
||||||
|
COLUMN_TYPE -> if (isParentRow) StringUtils.EMPTY else getFileType(file)
|
||||||
|
COLUMN_LAST_MODIFIED_TIME -> if (isParentRow) StringUtils.EMPTY else getLastModifiedTime(file)
|
||||||
|
COLUMN_ATTRS -> if (isParentRow) StringUtils.EMPTY else getAttrs(file)
|
||||||
|
COLUMN_OWNER -> StringUtils.EMPTY
|
||||||
|
else -> StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (file.fileSystem is LocalFileSystem) {
|
||||||
|
if (ExceptionUtils.getRootCause(e) is java.nio.file.NoSuchFileException) {
|
||||||
|
SwingUtilities.invokeLater { removeRow(row) }
|
||||||
|
return StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFileType(file: FileObject): String {
|
||||||
|
return if (SystemInfo.isWindows) NativeFileIcons.getIcon(file.name.baseName, file.isFile).second
|
||||||
|
else if (file.isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
||||||
|
else NativeFileIcons.getIcon(file.name.baseName, file.isFile).second
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileIcon(file: FileObject, width: Int = 16, height: Int = 16): Icon {
|
||||||
|
return if (SystemInfo.isWindows) NativeFileIcons.getIcon(file.name.baseName, file.isFile, width, height).first
|
||||||
|
else NativeFileIcons.getIcon(file.name.baseName, file.isFile).first
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileIcon(row: Int): Icon {
|
||||||
|
return getFileIcon(getFileObject(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastModifiedTime(file: FileObject): String {
|
||||||
|
if (file.content.lastModifiedTime < 1) return "-"
|
||||||
|
return DateFormatUtils.format(Date(file.content.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttrs(file: FileObject): String {
|
||||||
|
if (file.fileSystem is LocalFileSystem) return StringUtils.EMPTY
|
||||||
|
return PosixFilePermissions.toString(getFilePermissions(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilePermissions(file: FileObject): Set<PosixFilePermission> {
|
||||||
|
val permissions = file.content.getAttribute(MySftpFileObject.POSIX_FILE_PERMISSIONS)
|
||||||
|
as Int? ?: return emptySet()
|
||||||
|
return fromSftpPermissions(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDataVector(): Vector<Vector<Any>> {
|
override fun getDataVector(): Vector<Vector<Any>> {
|
||||||
return super.getDataVector()
|
return super.getDataVector()
|
||||||
}
|
}
|
||||||
@@ -100,14 +150,14 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttr(row: Int): Attr {
|
fun getFileObject(row: Int): FileObject {
|
||||||
return super.getValueAt(row, 0) as Attr
|
return super.getValueAt(row, 0) as FileObject
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPathNames(): Set<String> {
|
fun getPathNames(): Set<String> {
|
||||||
val names = linkedSetOf<String>()
|
val names = linkedSetOf<String>()
|
||||||
for (i in 0 until rowCount) {
|
for (i in 0 until rowCount) {
|
||||||
names.add(getAttr(i).name)
|
names.add(getFileObject(i).name.baseName)
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
@@ -129,144 +179,40 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reload(dir: Path, useFileHiding: Boolean) {
|
suspend fun reload(dir: FileObject, useFileHiding: Boolean) {
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Reloading {} , useFileHiding {}", dir, useFileHiding)
|
log.debug("Reloading {} , useFileHiding {}", dir, useFileHiding)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attrs = mutableListOf<Attr>()
|
val files = mutableListOf<FileObject>()
|
||||||
if (dir.parent != null) {
|
|
||||||
attrs.add(ParentAttr(dir.parent))
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Files.list(dir).use { paths ->
|
dir.refresh()
|
||||||
for (path in paths) {
|
for (file in dir.children) {
|
||||||
val attr = if (path is SftpPath) SftpAttr(path) else Attr(path)
|
if (useFileHiding && file.isHidden) continue
|
||||||
if (useFileHiding && attr.isHidden) continue
|
files.add(file)
|
||||||
attrs.add(attr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs.sortWith(compareBy<Attr> { !it.isDirectory }.thenComparing { a, b ->
|
files.sortWith(compareBy<FileObject> { !it.isFolder }.thenComparing { a, b ->
|
||||||
NativeStringComparator.getInstance().compare(
|
NativeStringComparator.getInstance().compare(
|
||||||
a.name,
|
a.name.baseName,
|
||||||
b.name
|
b.name.baseName
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
hasParent = dir.parent != null
|
||||||
|
if (hasParent) {
|
||||||
|
files.addFirst(dir.parent)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
while (rowCount > 0) removeRow(0)
|
while (rowCount > 0) removeRow(0)
|
||||||
attrs.forEach { addRow(arrayOf(it)) }
|
files.forEach { addRow(arrayOf(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
open class Attr(val path: Path) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 名称
|
|
||||||
*/
|
|
||||||
open val name by lazy { path.name }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件类型
|
|
||||||
*/
|
|
||||||
open val type by lazy {
|
|
||||||
if (path.fileSystem.isWindows()) NativeFileIcons.getIcon(name, isFile).second
|
|
||||||
else if (isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
|
||||||
else NativeFileIcons.getIcon(name, isFile).second
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 大小
|
|
||||||
*/
|
|
||||||
open val size by lazy { path.fileSize() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改时间
|
|
||||||
*/
|
|
||||||
open val modified by lazy { path.getLastModifiedTime().toMillis() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有者
|
|
||||||
*/
|
|
||||||
open val owner by lazy { StringUtils.EMPTY }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取操作系统图标
|
|
||||||
*/
|
|
||||||
open val icon by lazy { NativeFileIcons.getIcon(name, isFile).first }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件夹
|
|
||||||
*/
|
|
||||||
open val isDirectory by lazy { path.isDirectory() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件
|
|
||||||
*/
|
|
||||||
open val isFile by lazy { !isDirectory }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件夹
|
|
||||||
*/
|
|
||||||
open val isHidden by lazy { path.isHidden() }
|
|
||||||
|
|
||||||
open val isSymbolicLink by lazy { path.isSymbolicLink() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取权限
|
|
||||||
*/
|
|
||||||
open val permissions: String by lazy {
|
|
||||||
posixFilePermissions.let {
|
|
||||||
if (it.isNotEmpty()) PosixFilePermissions.toString(
|
|
||||||
it
|
|
||||||
) else StringUtils.EMPTY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
open val posixFilePermissions by lazy { if (path.fileSystem.isUnix()) path.getPosixFilePermissions() else emptySet() }
|
|
||||||
|
|
||||||
open fun toFile(): File {
|
|
||||||
if (path.fileSystem.isSFTP()) {
|
|
||||||
return File(path.absolutePathString())
|
|
||||||
}
|
|
||||||
return path.toFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParentAttr(path: Path) : Attr(path) {
|
|
||||||
override val name by lazy { ".." }
|
|
||||||
override val isDirectory = true
|
|
||||||
override val isFile = false
|
|
||||||
override val isHidden = false
|
|
||||||
override val permissions = StringUtils.EMPTY
|
|
||||||
override val modified = 0L
|
|
||||||
override val type = StringUtils.EMPTY
|
|
||||||
override val icon by lazy { NativeFileIcons.getFolderIcon() }
|
|
||||||
override val isSymbolicLink = false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SftpAttr(sftpPath: SftpPath) : Attr(sftpPath) {
|
|
||||||
private val attributes = sftpPath.attributes
|
|
||||||
|
|
||||||
override val isSymbolicLink = attributes.isSymbolicLink
|
|
||||||
override val isDirectory = if (isSymbolicLink) sftpPath.isDirectory() else attributes.isDirectory
|
|
||||||
override val isHidden = name.startsWith(".")
|
|
||||||
override val size = attributes.size
|
|
||||||
override val owner: String = StringUtils.defaultString(attributes.owner)
|
|
||||||
override val modified = attributes.modifyTime.toMillis()
|
|
||||||
override val permissions: String = PosixFilePermissions.toString(fromSftpPermissions(attributes.permissions))
|
|
||||||
override val posixFilePermissions = fromSftpPermissions(attributes.permissions)
|
|
||||||
|
|
||||||
override fun toFile(): File {
|
|
||||||
return File(path.absolutePathString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystemConfigBuilder
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -11,10 +12,11 @@ import kotlinx.coroutines.swing.Swing
|
|||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.sftp.client.SftpClientFactory
|
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import org.jdesktop.swingx.JXHyperlink
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -46,18 +48,16 @@ class SFTPFileSystemViewPanel(
|
|||||||
private var state = State.Initialized
|
private var state = State.Initialized
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val cardPanel = JPanel(cardLayout)
|
private val cardPanel = JPanel(cardLayout)
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val connectingPanel = ConnectingPanel()
|
private val connectingPanel = ConnectingPanel()
|
||||||
private val selectHostPanel = SelectHostPanel()
|
private val selectHostPanel = SelectHostPanel()
|
||||||
private val connectFailedPanel = ConnectFailedPanel()
|
private val connectFailedPanel = ConnectFailedPanel()
|
||||||
private val isDisposed = AtomicBoolean(false)
|
private val isDisposed = AtomicBoolean(false)
|
||||||
private val that = this
|
private val that = this
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
private var client: SshClient? = null
|
private var client: SshClient? = null
|
||||||
private var session: ClientSession? = null
|
private var session: ClientSession? = null
|
||||||
private var fileSystem: SftpFileSystem? = null
|
|
||||||
private var fileSystemPanel: FileSystemViewPanel? = null
|
private var fileSystemPanel: FileSystemViewPanel? = null
|
||||||
|
|
||||||
|
|
||||||
@@ -111,11 +111,17 @@ class SFTPFileSystemViewPanel(
|
|||||||
|
|
||||||
closeIO()
|
closeIO()
|
||||||
|
|
||||||
|
val mySftpFileSystem: FileSystem
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val owner = SwingUtilities.getWindowAncestor(that)
|
val owner = SwingUtilities.getWindowAncestor(that)
|
||||||
val client = SshClients.openClient(thisHost, owner).apply { client = this }
|
val client = SshClients.openClient(thisHost, owner).apply { client = this }
|
||||||
val session = SshClients.openSession(thisHost, client).apply { session = this }
|
val session = SshClients.openSession(thisHost, client).apply { session = this }
|
||||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
|
||||||
|
val options = FileSystemOptions()
|
||||||
|
MySftpFileSystemConfigBuilder.getInstance()
|
||||||
|
.setClientSession(options, session)
|
||||||
|
mySftpFileSystem = VFS.getManager().resolveFile("sftp:///", options).fileSystem
|
||||||
session.addCloseFutureListener { onClose() }
|
session.addCloseFutureListener { onClose() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
closeIO()
|
closeIO()
|
||||||
@@ -126,11 +132,10 @@ class SFTPFileSystemViewPanel(
|
|||||||
throw IllegalStateException("Closed")
|
throw IllegalStateException("Closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileSystem = this.fileSystem ?: return
|
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
state = State.Connected
|
state = State.Connected
|
||||||
val fileSystemPanel = FileSystemViewPanel(thisHost, fileSystem, transportManager, coroutineScope)
|
val fileSystemPanel = FileSystemViewPanel(thisHost, mySftpFileSystem, transportManager, coroutineScope)
|
||||||
cardPanel.add(fileSystemPanel, State.Connected.name)
|
cardPanel.add(fileSystemPanel, State.Connected.name)
|
||||||
cardLayout.show(cardPanel, State.Connected.name)
|
cardLayout.show(cardPanel, State.Connected.name)
|
||||||
that.fileSystemPanel = fileSystemPanel
|
that.fileSystemPanel = fileSystemPanel
|
||||||
@@ -157,7 +162,6 @@ class SFTPFileSystemViewPanel(
|
|||||||
fileSystemPanel?.let { Disposer.dispose(it) }
|
fileSystemPanel?.let { Disposer.dispose(it) }
|
||||||
fileSystemPanel = null
|
fileSystemPanel = null
|
||||||
|
|
||||||
runCatching { IOUtils.closeQuietly(fileSystem) }
|
|
||||||
runCatching { IOUtils.closeQuietly(session) }
|
runCatching { IOUtils.closeQuietly(session) }
|
||||||
runCatching { IOUtils.closeQuietly(client) }
|
runCatching { IOUtils.closeQuietly(client) }
|
||||||
|
|
||||||
|
|||||||
18
src/main/kotlin/app/termora/sftp/SFTPKit.kt
Normal file
18
src/main/kotlin/app/termora/sftp/SFTPKit.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.termora.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
fun FileObject.absolutePathString(): String {
|
||||||
|
var text = name.path
|
||||||
|
if (this is LocalFile && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
text = this.name.toString()
|
||||||
|
text = StringUtils.removeStart(text, "file:///")
|
||||||
|
text = StringUtils.replace(text, "/", File.separator)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
@@ -5,31 +5,34 @@ import app.termora.actions.DataProvider
|
|||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import okio.withLock
|
import okio.withLock
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Path
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
fun FileSystem.isSFTP() = this is SftpFileSystem
|
|
||||||
fun FileSystem.isUnix() = isLocal() && SystemUtils.IS_OS_UNIX
|
|
||||||
fun FileSystem.isWindows() = isLocal() && SystemUtils.IS_OS_WINDOWS
|
|
||||||
fun FileSystem.isLocal() = StringUtils.startsWithAny(javaClass.name, "java", "sun")
|
|
||||||
|
|
||||||
class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val transportTable = TransportTable()
|
private val transportTable = TransportTable()
|
||||||
private val transportManager get() = transportTable.model
|
private val transportManager get() = transportTable.model
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val leftComponent = SFTPTabbed(transportManager)
|
private val leftComponent = SFTPTabbed(transportManager)
|
||||||
private val rightComponent = SFTPTabbed(transportManager)
|
private val rightComponent = SFTPTabbed(transportManager)
|
||||||
|
private val localHost = Host(
|
||||||
|
id = "local",
|
||||||
|
name = I18n.getString("termora.transport.local"),
|
||||||
|
protocol = Protocol.Local,
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initViews()
|
initViews()
|
||||||
@@ -87,11 +90,10 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
leftComponent.addTab(
|
leftComponent.addTab(
|
||||||
I18n.getString("termora.transport.local"),
|
I18n.getString("termora.transport.local"),
|
||||||
FileSystemViewPanel(
|
FileSystemViewPanel(
|
||||||
Host(
|
localHost,
|
||||||
id = "local",
|
VFS.getManager().resolveFile("file:///${SystemUtils.USER_HOME}").fileSystem,
|
||||||
name = I18n.getString("termora.transport.local"),
|
transportManager,
|
||||||
protocol = Protocol.Local,
|
coroutineScope
|
||||||
), FileSystems.getDefault(), transportManager
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
leftComponent.setTabClosable(0, false)
|
leftComponent.setTabClosable(0, false)
|
||||||
@@ -165,9 +167,9 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
*/
|
*/
|
||||||
fun addTransport(
|
fun addTransport(
|
||||||
source: JComponent,
|
source: JComponent,
|
||||||
sourceWorkdir: Path?,
|
sourceWorkdir: FileObject?,
|
||||||
target: FileSystemViewPanel,
|
target: FileSystemViewPanel,
|
||||||
targetWorkdir: Path?,
|
targetWorkdir: FileObject?,
|
||||||
transport: Transport
|
transport: Transport
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
@@ -175,15 +177,12 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
as? FileSystemViewPanel ?: return false
|
as? FileSystemViewPanel ?: return false
|
||||||
val targetPanel = target as? FileSystemViewPanel ?: return false
|
val targetPanel = target as? FileSystemViewPanel ?: return false
|
||||||
if (sourcePanel.isDisposed || targetPanel.isDisposed) return false
|
if (sourcePanel.isDisposed || targetPanel.isDisposed) return false
|
||||||
val myTargetWorkdir = (targetWorkdir ?: targetPanel.getWorkdir()).absolutePathString()
|
val myTargetWorkdir = (targetWorkdir ?: targetPanel.getWorkdir())
|
||||||
val mySourceWorkdir = (sourceWorkdir ?: sourcePanel.getWorkdir()).absolutePathString()
|
val mySourceWorkdir = (sourceWorkdir ?: sourcePanel.getWorkdir())
|
||||||
val targetFileSystem = targetPanel.fileSystem
|
val sourcePath = transport.source
|
||||||
val sourcePath = transport.source.absolutePathString()
|
|
||||||
|
|
||||||
transport.target = targetFileSystem.getPath(
|
val relativeName = mySourceWorkdir.name.getRelativeName(sourcePath.name)
|
||||||
myTargetWorkdir,
|
transport.target = myTargetWorkdir.resolveFile(relativeName)
|
||||||
StringUtils.removeStart(sourcePath, mySourceWorkdir)
|
|
||||||
)
|
|
||||||
|
|
||||||
return transportManager.addTransport(transport)
|
return transportManager.addTransport(transport)
|
||||||
|
|
||||||
@@ -212,4 +211,8 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return dataProviderSupport.getData(dataKey)
|
return dataProviderSupport.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ import javax.swing.JButton
|
|||||||
import javax.swing.JToolBar
|
import javax.swing.JToolBar
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
|
|||||||
@@ -6,18 +6,13 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.net.io.Util
|
import org.apache.commons.net.io.Util
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.file.Files
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.attribute.BasicFileAttributeView
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.exists
|
|
||||||
import kotlin.io.path.getLastModifiedTime
|
|
||||||
import kotlin.io.path.name
|
|
||||||
|
|
||||||
enum class TransportStatus {
|
enum class TransportStatus {
|
||||||
Ready,
|
Ready,
|
||||||
@@ -48,12 +43,19 @@ class Transport(
|
|||||||
/**
|
/**
|
||||||
* 源
|
* 源
|
||||||
*/
|
*/
|
||||||
val source: Path,
|
val source: FileObject,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 目标
|
* 目标
|
||||||
*/
|
*/
|
||||||
var target: Path,
|
var target: FileObject,
|
||||||
|
/**
|
||||||
|
* 仅对文件生效,切只有两个选项
|
||||||
|
*
|
||||||
|
* 1. [StandardOpenOption.APPEND]
|
||||||
|
* 2. [StandardOpenOption.TRUNCATE_EXISTING]
|
||||||
|
*/
|
||||||
|
var mode: StandardOpenOption = StandardOpenOption.TRUNCATE_EXISTING
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -154,7 +156,7 @@ class Transport(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (!target.exists()) {
|
if (!target.exists()) {
|
||||||
target.createDirectories()
|
target.createFolder()
|
||||||
}
|
}
|
||||||
} catch (e: FileAlreadyExistsException) {
|
} catch (e: FileAlreadyExistsException) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
@@ -169,8 +171,8 @@ class Transport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val input = Files.newInputStream(source)
|
val input = source.content.inputStream
|
||||||
val output = Files.newOutputStream(target)
|
val output = target.content.getOutputStream(mode == StandardOpenOption.APPEND)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -209,8 +211,7 @@ class Transport(
|
|||||||
private fun preserveModificationTime() {
|
private fun preserveModificationTime() {
|
||||||
// 设置修改时间
|
// 设置修改时间
|
||||||
if (isPreserveModificationTime) {
|
if (isPreserveModificationTime) {
|
||||||
Files.getFileAttributeView(target, BasicFileAttributeView::class.java)
|
target.content.lastModifiedTime = source.content.lastModifiedTime
|
||||||
.setTimes(source.getLastModifiedTime(), source.getLastModifiedTime(), null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package app.termora.sftp
|
|||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.formatBytes
|
import app.termora.formatBytes
|
||||||
import app.termora.formatSeconds
|
import app.termora.formatSeconds
|
||||||
import org.apache.commons.io.file.PathUtils
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystemConfigBuilder
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode(transport) {
|
class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode(transport) {
|
||||||
val transport get() = userObject as Transport
|
val transport get() = userObject as Transport
|
||||||
@@ -20,7 +19,7 @@ class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode
|
|||||||
(transport.filesize.get() - transport.transferredFilesize.get()) / speed else 0
|
(transport.filesize.get() - transport.transferredFilesize.get()) / speed else 0
|
||||||
|
|
||||||
return when (column) {
|
return when (column) {
|
||||||
TransportTableModel.COLUMN_NAME -> PathUtils.getFileNameString(transport.source)
|
TransportTableModel.COLUMN_NAME -> transport.source.name.baseName
|
||||||
TransportTableModel.COLUMN_STATUS -> formatStatus(transport)
|
TransportTableModel.COLUMN_STATUS -> formatStatus(transport)
|
||||||
TransportTableModel.COLUMN_SIZE -> size()
|
TransportTableModel.COLUMN_SIZE -> size()
|
||||||
TransportTableModel.COLUMN_SPEED -> if (isProcessing) formatBytes(speed) + "/s" else "-"
|
TransportTableModel.COLUMN_SPEED -> if (isProcessing) formatBytes(speed) + "/s" else "-"
|
||||||
@@ -31,12 +30,14 @@ class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatPath(path: Path): String {
|
private fun formatPath(file: FileObject): String {
|
||||||
if (path.fileSystem.isSFTP()) {
|
if (file.fileSystem is MySftpFileSystem) {
|
||||||
val hostname = ((path.fileSystem as SftpFileSystem).session as JGitClientSession).hostConfigEntry.hostName
|
val session = MySftpFileSystemConfigBuilder.getInstance()
|
||||||
return hostname + ":" + path.absolutePathString()
|
.getClientSession(file.fileSystem.fileSystemOptions) as JGitClientSession
|
||||||
|
val hostname = session.hostConfigEntry.hostName
|
||||||
|
return hostname + ":" + file.name.path
|
||||||
}
|
}
|
||||||
return path.toUri().scheme + ":" + path.absolutePathString()
|
return file.name.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatStatus(transport: Transport): String {
|
private fun formatStatus(transport: Transport): String {
|
||||||
|
|||||||
273
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileObject.kt
Normal file
273
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileObject.kt
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package app.termora.vfs2.sftp
|
||||||
|
|
||||||
|
import app.termora.sftp.FileSystemViewTableModel
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileSystemException
|
||||||
|
import org.apache.commons.vfs2.FileType
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractFileObject
|
||||||
|
import org.apache.sshd.sftp.client.SftpClient
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpPath
|
||||||
|
import org.apache.sshd.sftp.client.fs.WithFileAttributes
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.io.path.*
|
||||||
|
|
||||||
|
class MySftpFileObject(
|
||||||
|
private val sftpFileSystem: SftpFileSystem,
|
||||||
|
fileName: AbstractFileName,
|
||||||
|
fileSystem: MySftpFileSystem
|
||||||
|
) : AbstractFileObject<MySftpFileSystem>(fileName, fileSystem) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MySftpFileObject::class.java)
|
||||||
|
|
||||||
|
const val POSIX_FILE_PERMISSIONS = "PosixFilePermissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _attributes: SftpClient.Attributes? = null
|
||||||
|
private val isInitialized = AtomicBoolean(false)
|
||||||
|
private val path by lazy { sftpFileSystem.getPath(fileName.path) }
|
||||||
|
private val attributes = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
override fun doGetContentSize(): Long {
|
||||||
|
val attributes = getAttributes()
|
||||||
|
if (attributes == null || !attributes.flags.contains(SftpClient.Attribute.Size)) {
|
||||||
|
throw FileSystemException("vfs.provider.sftp/unknown-size.error")
|
||||||
|
}
|
||||||
|
return attributes.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doGetType(): FileType {
|
||||||
|
val attributes = getAttributes() ?: return FileType.IMAGINARY
|
||||||
|
return if (attributes.isDirectory)
|
||||||
|
FileType.FOLDER
|
||||||
|
else if (attributes.isRegularFile)
|
||||||
|
FileType.FILE
|
||||||
|
else if (attributes.isSymbolicLink) {
|
||||||
|
val e = path.readSymbolicLink()
|
||||||
|
if (e is SftpPath && e.attributes != null) {
|
||||||
|
if (e.attributes.isDirectory) {
|
||||||
|
FileType.FOLDER
|
||||||
|
} else {
|
||||||
|
FileType.FILE
|
||||||
|
}
|
||||||
|
} else if (e.isDirectory()) {
|
||||||
|
FileType.FOLDER
|
||||||
|
} else {
|
||||||
|
FileType.FILE
|
||||||
|
}
|
||||||
|
} else FileType.IMAGINARY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doListChildren(): Array<String>? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doListChildrenResolved(): Array<FileObject>? {
|
||||||
|
if (isFile) return null
|
||||||
|
|
||||||
|
val children = mutableListOf<FileObject>()
|
||||||
|
|
||||||
|
Files.list(path).use { files ->
|
||||||
|
for (file in files) {
|
||||||
|
val fo = resolveFile(file.name)
|
||||||
|
if (file is WithFileAttributes && fo is MySftpFileObject) {
|
||||||
|
if (fo.isInitialized.compareAndSet(false, true)) {
|
||||||
|
fo.setAttributes(file.attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.add(fo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return children.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doGetOutputStream(bAppend: Boolean): OutputStream {
|
||||||
|
if (bAppend) {
|
||||||
|
return path.outputStream(StandardOpenOption.WRITE, StandardOpenOption.APPEND)
|
||||||
|
}
|
||||||
|
return path.outputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doGetInputStream(bufferSize: Int): InputStream {
|
||||||
|
return path.inputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCreateFolder() {
|
||||||
|
Files.createDirectories(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doIsExecutable(): Boolean {
|
||||||
|
val permissions = getPermissions()
|
||||||
|
return permissions.contains(PosixFilePermission.GROUP_EXECUTE) ||
|
||||||
|
permissions.contains(PosixFilePermission.OWNER_EXECUTE) ||
|
||||||
|
permissions.contains(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doIsReadable(): Boolean {
|
||||||
|
val permissions = getPermissions()
|
||||||
|
return permissions.contains(PosixFilePermission.GROUP_READ) ||
|
||||||
|
permissions.contains(PosixFilePermission.OWNER_READ) ||
|
||||||
|
permissions.contains(PosixFilePermission.OTHERS_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doIsWriteable(): Boolean {
|
||||||
|
val permissions = getPermissions()
|
||||||
|
return permissions.contains(PosixFilePermission.GROUP_WRITE) ||
|
||||||
|
permissions.contains(PosixFilePermission.OWNER_WRITE) ||
|
||||||
|
permissions.contains(PosixFilePermission.OTHERS_WRITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doRename(newFile: FileObject) {
|
||||||
|
if (newFile !is MySftpFileObject) {
|
||||||
|
throw FileSystemException("vfs.provider/rename-not-supported.error")
|
||||||
|
}
|
||||||
|
Files.move(path, newFile.path, StandardCopyOption.ATOMIC_MOVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveTo(destFile: FileObject) {
|
||||||
|
if (canRenameTo(destFile)) {
|
||||||
|
doRename(destFile)
|
||||||
|
} else {
|
||||||
|
throw FileSystemException("vfs.provider/rename-not-supported.error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doDelete() {
|
||||||
|
sftpFileSystem.client.use { deleteRecursivelySFTP(path, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化删除效率,采用一个连接
|
||||||
|
*/
|
||||||
|
private fun deleteRecursivelySFTP(path: SftpPath, sftpClient: SftpClient) {
|
||||||
|
val isDirectory = if (path.attributes != null) path.attributes.isDirectory else path.isDirectory()
|
||||||
|
if (isDirectory) {
|
||||||
|
for (e in sftpClient.readDir(path.toString())) {
|
||||||
|
if (e.filename == ".." || e.filename == ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (e.attributes.isDirectory) {
|
||||||
|
deleteRecursivelySFTP(path.resolve(e.filename), sftpClient)
|
||||||
|
} else {
|
||||||
|
sftpClient.remove(path.resolve(e.filename).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sftpClient.rmdir(path.toString())
|
||||||
|
} else {
|
||||||
|
sftpClient.remove(path.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doSetExecutable(executable: Boolean, ownerOnly: Boolean): Boolean {
|
||||||
|
val permissions = getPermissions().toMutableSet()
|
||||||
|
permissions.add(PosixFilePermission.OWNER_EXECUTE)
|
||||||
|
if (ownerOnly) {
|
||||||
|
permissions.remove(PosixFilePermission.OTHERS_EXECUTE)
|
||||||
|
permissions.remove(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
}
|
||||||
|
Files.setPosixFilePermissions(path, permissions)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doSetReadable(readable: Boolean, ownerOnly: Boolean): Boolean {
|
||||||
|
val permissions = getPermissions().toMutableSet()
|
||||||
|
permissions.add(PosixFilePermission.OWNER_READ)
|
||||||
|
if (ownerOnly) {
|
||||||
|
permissions.remove(PosixFilePermission.OTHERS_READ)
|
||||||
|
permissions.remove(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
}
|
||||||
|
Files.setPosixFilePermissions(path, permissions)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doSetWritable(writable: Boolean, ownerOnly: Boolean): Boolean {
|
||||||
|
val permissions = getPermissions().toMutableSet()
|
||||||
|
permissions.add(PosixFilePermission.OWNER_WRITE)
|
||||||
|
if (ownerOnly) {
|
||||||
|
permissions.remove(PosixFilePermission.OTHERS_WRITE)
|
||||||
|
permissions.remove(PosixFilePermission.GROUP_WRITE)
|
||||||
|
}
|
||||||
|
Files.setPosixFilePermissions(path, permissions)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doSetLastModifiedTime(modtime: Long): Boolean {
|
||||||
|
Files.setLastModifiedTime(path, FileTime.fromMillis(modtime))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doDetach() {
|
||||||
|
setAttributes(null)
|
||||||
|
isInitialized.compareAndSet(true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doIsHidden(): Boolean {
|
||||||
|
return name.baseName.startsWith(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doGetAttributes(): MutableMap<String, Any> {
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doGetLastModifiedTime(): Long {
|
||||||
|
val attributes = getAttributes()
|
||||||
|
if (attributes == null || !attributes.flags.contains(SftpClient.Attribute.ModifyTime)) {
|
||||||
|
throw FileSystemException("vfs.provider.sftp/unknown-modtime.error")
|
||||||
|
}
|
||||||
|
return attributes.modifyTime.toMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doSetAttribute(attrName: String, value: Any) {
|
||||||
|
attributes[attrName] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doIsSymbolicLink(): Boolean {
|
||||||
|
return getAttributes()?.isSymbolicLink == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPosixFilePermissions(permissions: Set<PosixFilePermission>) {
|
||||||
|
path.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttributes(): SftpClient.Attributes? {
|
||||||
|
if (isInitialized.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
val attributes = sftpFileSystem.provider()
|
||||||
|
.readRemoteAttributes(sftpFileSystem.provider().toSftpPath(path))
|
||||||
|
setAttributes(attributes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAttributes(attributes: SftpClient.Attributes?) {
|
||||||
|
if (attributes == null) {
|
||||||
|
doGetAttributes().remove(POSIX_FILE_PERMISSIONS)
|
||||||
|
} else {
|
||||||
|
doSetAttribute(POSIX_FILE_PERMISSIONS, attributes.permissions)
|
||||||
|
}
|
||||||
|
this._attributes = attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPermissions(): Set<PosixFilePermission> {
|
||||||
|
return FileSystemViewTableModel.fromSftpPermissions(getAttributes()?.permissions ?: return setOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
45
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileProvider.kt
Normal file
45
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileProvider.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package app.termora.vfs2.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.Capability
|
||||||
|
import org.apache.commons.vfs2.FileName
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||||
|
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||||
|
|
||||||
|
class MySftpFileProvider : AbstractOriginatingFileProvider() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val capabilities = listOf(
|
||||||
|
Capability.CREATE,
|
||||||
|
Capability.DELETE,
|
||||||
|
Capability.RENAME,
|
||||||
|
Capability.GET_TYPE,
|
||||||
|
Capability.LIST_CHILDREN,
|
||||||
|
Capability.READ_CONTENT,
|
||||||
|
Capability.URI,
|
||||||
|
Capability.WRITE_CONTENT,
|
||||||
|
Capability.GET_LAST_MODIFIED,
|
||||||
|
Capability.SET_LAST_MODIFIED_FILE,
|
||||||
|
Capability.RANDOM_ACCESS_READ,
|
||||||
|
Capability.APPEND_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCapabilities(): Collection<Capability> {
|
||||||
|
return MySftpFileProvider.capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCreateFileSystem(rootFileName: FileName, fileSystemOptions: FileSystemOptions): FileSystem {
|
||||||
|
val clientSession = MySftpFileSystemConfigBuilder.getInstance()
|
||||||
|
.getClientSession(fileSystemOptions)
|
||||||
|
if (clientSession == null) {
|
||||||
|
throw IllegalArgumentException("client session not found")
|
||||||
|
}
|
||||||
|
return MySftpFileSystem(
|
||||||
|
SftpClientFactory.instance().createSftpFileSystem(clientSession),
|
||||||
|
rootFileName,
|
||||||
|
fileSystemOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileSystem.kt
Normal file
34
src/main/kotlin/app/termora/vfs2/sftp/MySftpFileSystem.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package app.termora.vfs2.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.Capability
|
||||||
|
import org.apache.commons.vfs2.FileName
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractFileSystem
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
|
class MySftpFileSystem(
|
||||||
|
private val sftpFileSystem: SftpFileSystem,
|
||||||
|
rootName: FileName,
|
||||||
|
fileSystemOptions: FileSystemOptions
|
||||||
|
) : AbstractFileSystem(rootName, null, fileSystemOptions) {
|
||||||
|
|
||||||
|
override fun addCapabilities(caps: MutableCollection<Capability>) {
|
||||||
|
caps.addAll(MySftpFileProvider.capabilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFile(name: AbstractFileName): FileObject {
|
||||||
|
return MySftpFileObject(sftpFileSystem, name, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultDir(): String {
|
||||||
|
return sftpFileSystem.defaultDir.absolutePathString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getClientSession(): ClientSession {
|
||||||
|
return sftpFileSystem.session
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.termora.vfs2.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.FileSystemConfigBuilder
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
|
||||||
|
class MySftpFileSystemConfigBuilder : FileSystemConfigBuilder() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val INSTANCE by lazy { MySftpFileSystemConfigBuilder() }
|
||||||
|
fun getInstance(): MySftpFileSystemConfigBuilder {
|
||||||
|
return INSTANCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConfigClass(): Class<out FileSystem> {
|
||||||
|
return MySftpFileSystem::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setClientSession(options: FileSystemOptions, session: ClientSession) {
|
||||||
|
setParam(options, "session", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getClientSession(options: FileSystemOptions): ClientSession? {
|
||||||
|
return getParam(options, "session")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -320,6 +320,7 @@ termora.transport.sftp.status.failed=Failed
|
|||||||
termora.transport.sftp.already-exists.message1=This folder already contains an object named as below
|
termora.transport.sftp.already-exists.message1=This folder already contains an object named as below
|
||||||
termora.transport.sftp.already-exists.message2=Select a task to do
|
termora.transport.sftp.already-exists.message2=Select a task to do
|
||||||
termora.transport.sftp.already-exists.overwrite=Overwrite
|
termora.transport.sftp.already-exists.overwrite=Overwrite
|
||||||
|
termora.transport.sftp.already-exists.append=Append
|
||||||
termora.transport.sftp.already-exists.skip=Skip
|
termora.transport.sftp.already-exists.skip=Skip
|
||||||
termora.transport.sftp.already-exists.apply-all=Apply all
|
termora.transport.sftp.already-exists.apply-all=Apply all
|
||||||
termora.transport.sftp.already-exists.name=Name
|
termora.transport.sftp.already-exists.name=Name
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ termora.transport.sftp.status.failed=已失败
|
|||||||
termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象
|
termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象
|
||||||
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
||||||
termora.transport.sftp.already-exists.overwrite=覆盖
|
termora.transport.sftp.already-exists.overwrite=覆盖
|
||||||
|
termora.transport.sftp.already-exists.append=追加
|
||||||
termora.transport.sftp.already-exists.skip=跳过
|
termora.transport.sftp.already-exists.skip=跳过
|
||||||
termora.transport.sftp.already-exists.apply-all=应用全部
|
termora.transport.sftp.already-exists.apply-all=应用全部
|
||||||
termora.transport.sftp.already-exists.name=名称
|
termora.transport.sftp.already-exists.name=名称
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ termora.transport.sftp.status.failed=已失敗
|
|||||||
termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象
|
termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象
|
||||||
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
||||||
termora.transport.sftp.already-exists.overwrite=覆蓋
|
termora.transport.sftp.already-exists.overwrite=覆蓋
|
||||||
|
termora.transport.sftp.already-exists.append=追加
|
||||||
termora.transport.sftp.already-exists.skip=跳過
|
termora.transport.sftp.already-exists.skip=跳過
|
||||||
termora.transport.sftp.already-exists.apply-all=應用全部
|
termora.transport.sftp.already-exists.apply-all=應用全部
|
||||||
termora.transport.sftp.already-exists.name=名稱
|
termora.transport.sftp.already-exists.name=名稱
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ class SFTPTest : SSHDTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
|
|
||||||
val client = SshClients.openClient(host)
|
val session = newClientSession()
|
||||||
val session = SshClients.openSession(host, client)
|
|
||||||
assertTrue(session.isOpen)
|
assertTrue(session.isOpen)
|
||||||
|
|
||||||
|
|
||||||
val fileSystem = DefaultSftpClientFactory.INSTANCE.createSftpFileSystem(session)
|
val fileSystem = DefaultSftpClientFactory.INSTANCE.createSftpFileSystem(session)
|
||||||
for (path in Files.list(fileSystem.rootDirectories.first())) {
|
for (path in Files.list(fileSystem.rootDirectories.first())) {
|
||||||
println(path)
|
println(path)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.testcontainers.containers.GenericContainer
|
import org.testcontainers.containers.GenericContainer
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
|
||||||
abstract class SSHDTest {
|
abstract class SSHDTest {
|
||||||
@@ -17,8 +19,8 @@ abstract class SSHDTest {
|
|||||||
.withEnv("SUDO_ACCESS", "true")
|
.withEnv("SUDO_ACCESS", "true")
|
||||||
.withExposedPorts(2222)
|
.withExposedPorts(2222)
|
||||||
|
|
||||||
protected val host by lazy {
|
protected val host
|
||||||
Host(
|
get() = Host(
|
||||||
name = sshd.containerName,
|
name = sshd.containerName,
|
||||||
protocol = Protocol.SSH,
|
protocol = Protocol.SSH,
|
||||||
host = "127.0.0.1",
|
host = "127.0.0.1",
|
||||||
@@ -26,7 +28,6 @@ abstract class SSHDTest {
|
|||||||
username = "foo",
|
username = "foo",
|
||||||
authentication = Authentication.No.copy(type = AuthenticationType.Password, password = "pass"),
|
authentication = Authentication.No.copy(type = AuthenticationType.Password, password = "pass"),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
@@ -38,4 +39,11 @@ abstract class SSHDTest {
|
|||||||
fun teardown() {
|
fun teardown() {
|
||||||
sshd.stop()
|
sshd.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newClientSession(): ClientSession {
|
||||||
|
val client = SshClients.openClient(host)
|
||||||
|
val session = SshClients.openSession(host, client)
|
||||||
|
assertTrue(session.isOpen)
|
||||||
|
return session
|
||||||
|
}
|
||||||
}
|
}
|
||||||
114
src/test/kotlin/app/termora/vfs2/sftp/MySftpFileProviderTest.kt
Normal file
114
src/test/kotlin/app/termora/vfs2/sftp/MySftpFileProviderTest.kt
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package app.termora.vfs2.sftp
|
||||||
|
|
||||||
|
import app.termora.SSHDTest
|
||||||
|
import app.termora.toSimpleString
|
||||||
|
import org.apache.commons.vfs2.*
|
||||||
|
import org.apache.commons.vfs2.impl.DefaultFileSystemManager
|
||||||
|
import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider
|
||||||
|
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class MySftpFileProviderTest : SSHDTest() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
val fileSystemManager = DefaultFileSystemManager()
|
||||||
|
fileSystemManager.addProvider("sftp", MySftpFileProvider())
|
||||||
|
fileSystemManager.addProvider("file", DefaultLocalFileProvider())
|
||||||
|
fileSystemManager.init()
|
||||||
|
VFS.setManager(fileSystemManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSetExecutable() {
|
||||||
|
val file = newFileObject("/config/test.txt")
|
||||||
|
file.createFile()
|
||||||
|
file.refresh()
|
||||||
|
assertFalse(file.isExecutable)
|
||||||
|
file.setExecutable(true, false)
|
||||||
|
file.refresh()
|
||||||
|
assertTrue(file.isExecutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateFile() {
|
||||||
|
val file = newFileObject("/config/test.txt")
|
||||||
|
assertFalse(file.exists())
|
||||||
|
file.createFile()
|
||||||
|
assertTrue(file.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWriteAndReadFile() {
|
||||||
|
val file = newFileObject("/config/test.txt")
|
||||||
|
file.createFile()
|
||||||
|
assertFalse(file.content.isOpen)
|
||||||
|
|
||||||
|
val os = file.content.outputStream
|
||||||
|
os.write("test".toByteArray())
|
||||||
|
os.flush()
|
||||||
|
assertTrue(file.content.isOpen)
|
||||||
|
|
||||||
|
os.close()
|
||||||
|
assertFalse(file.content.isOpen)
|
||||||
|
|
||||||
|
val input = file.content.inputStream
|
||||||
|
assertEquals("test", String(input.readAllBytes()))
|
||||||
|
assertTrue(file.content.isOpen)
|
||||||
|
input.close()
|
||||||
|
assertFalse(file.content.isOpen)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateFolder() {
|
||||||
|
val file = newFileObject("/config/test")
|
||||||
|
assertFalse(file.exists())
|
||||||
|
file.createFolder()
|
||||||
|
assertTrue(file.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSftpClient() {
|
||||||
|
val session = newClientSession()
|
||||||
|
val client = SftpClientFactory.instance().createSftpClient(session)
|
||||||
|
assertTrue(client.isOpen)
|
||||||
|
session.close()
|
||||||
|
assertFalse(client.isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCopy() {
|
||||||
|
val file = newFileObject("/config/sshd.pid")
|
||||||
|
val filepath = File("build", UUID.randomUUID().toSimpleString())
|
||||||
|
val localFile = getVFS().resolveFile("file://${filepath.absolutePath}")
|
||||||
|
|
||||||
|
localFile.copyFrom(file, Selectors.SELECT_ALL)
|
||||||
|
assertEquals(
|
||||||
|
file.content.getString(Charsets.UTF_8),
|
||||||
|
localFile.content.getString(Charsets.UTF_8)
|
||||||
|
)
|
||||||
|
|
||||||
|
localFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVFS(): FileSystemManager {
|
||||||
|
return VFS.getManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newFileObject(path: String): FileObject {
|
||||||
|
val vfs = getVFS()
|
||||||
|
val fileSystemOptions = FileSystemOptions()
|
||||||
|
MySftpFileSystemConfigBuilder.getInstance().setClientSession(fileSystemOptions, newClientSession())
|
||||||
|
return vfs.resolveFile("sftp://${path}", fileSystemOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user