mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: SFTP file exists and prompts to overwrite (#426)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
kotlin = "2.1.10"
|
||||
kotlin = "2.1.20"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.2"
|
||||
tinylog = "2.7.0"
|
||||
|
||||
@@ -93,6 +93,7 @@ object Icons {
|
||||
val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") }
|
||||
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||
val file by lazy { DynamicIcon("icons/file.svg", "icons/file_dark.svg") }
|
||||
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||
|
||||
@@ -29,6 +29,7 @@ object OptionPane {
|
||||
icon: Icon? = null,
|
||||
options: Array<Any>? = null,
|
||||
initialValue: Any? = null,
|
||||
customizeDialog: (JDialog) -> Unit = {},
|
||||
): Int {
|
||||
|
||||
val panel = if (message is JComponent) {
|
||||
@@ -47,6 +48,9 @@ object OptionPane {
|
||||
override fun selectInitialValue() {
|
||||
super.selectInitialValue()
|
||||
if (message is JComponent) {
|
||||
if (message.getClientProperty("SKIP_requestFocusInWindow") == true) {
|
||||
return
|
||||
}
|
||||
message.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
@@ -58,6 +62,7 @@ object OptionPane {
|
||||
}
|
||||
})
|
||||
dialog.setLocationRelativeTo(parentComponent)
|
||||
customizeDialog.invoke(dialog)
|
||||
dialog.isVisible = true
|
||||
dialog.dispose()
|
||||
val selectedValue = pane.value
|
||||
|
||||
@@ -4,13 +4,17 @@ import app.termora.*
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import org.apache.sshd.sftp.client.SftpClient
|
||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
||||
@@ -18,6 +22,7 @@ import org.apache.sshd.sftp.client.fs.SftpPosixFileAttributes
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
@@ -37,6 +42,7 @@ import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import kotlin.collections.ArrayDeque
|
||||
import kotlin.io.path.*
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
@@ -556,22 +562,6 @@ class FileSystemViewTable(
|
||||
fileSystemViewPanel.newFolderOrFile(text, isFile)
|
||||
}
|
||||
|
||||
private fun transfer(
|
||||
attrs: Array<FileSystemViewTableModel.Attr>,
|
||||
fromLocalSystem: Boolean = false,
|
||||
targetWorkdir: Path? = null
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
doTransfer(attrs, fromLocalSystem, targetWorkdir)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deletePaths(paths: Array<Path>, rm: Boolean = false) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
@@ -657,6 +647,183 @@ class FileSystemViewTable(
|
||||
|
||||
}
|
||||
|
||||
private fun transfer(
|
||||
attrs: Array<FileSystemViewTableModel.Attr>,
|
||||
fromLocalSystem: Boolean = false,
|
||||
targetWorkdir: Path? = null
|
||||
) {
|
||||
|
||||
assertEventDispatchThread()
|
||||
|
||||
val target = sftpPanel.getTarget(table) ?: return
|
||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||
var overwriteAll = false
|
||||
|
||||
for (attr in attrs) {
|
||||
|
||||
if (!overwriteAll) {
|
||||
val targetAttr = 0.rangeUntil(table.model.rowCount).map { table.model.getAttr(it) }
|
||||
.find { it.name == attr.name }
|
||||
if (targetAttr != null) {
|
||||
val askTransfer = askTransfer(attr, targetAttr)
|
||||
if (askTransfer.option != JOptionPane.YES_OPTION) {
|
||||
continue
|
||||
}
|
||||
if (askTransfer.action == AskTransfer.Action.Skip) {
|
||||
if (askTransfer.applyAll) break
|
||||
continue
|
||||
} else if (askTransfer.action == AskTransfer.Action.Overwrite) {
|
||||
overwriteAll = askTransfer.applyAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
doTransfer(arrayOf(attr), fromLocalSystem, targetWorkdir)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class AskTransfer(
|
||||
val option: Int,
|
||||
val action: Action,
|
||||
val applyAll: Boolean
|
||||
) {
|
||||
enum class Action {
|
||||
Overwrite,
|
||||
Skip
|
||||
}
|
||||
}
|
||||
|
||||
private fun askTransfer(
|
||||
sourceAttr: FileSystemViewTableModel.Attr,
|
||||
targetAttr: FileSystemViewTableModel.Attr
|
||||
): AskTransfer {
|
||||
val formMargin = "7dlu"
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, 2dlu, left:pref",
|
||||
"pref, 12dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val iconSize = 36
|
||||
|
||||
val targetIcon = if (SystemInfo.isWindows)
|
||||
NativeFileIcons.getIcon(targetAttr.name, targetAttr.isFile, iconSize, iconSize).first
|
||||
else if (targetAttr.isDirectory) {
|
||||
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||
} else {
|
||||
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
||||
}
|
||||
|
||||
val sourceIcon = if (SystemInfo.isWindows)
|
||||
NativeFileIcons.getIcon(sourceAttr.name, sourceAttr.isFile, iconSize, iconSize).first
|
||||
else if (sourceAttr.isDirectory) {
|
||||
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||
} else {
|
||||
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(
|
||||
Date(targetAttr.modified),
|
||||
"yyyy/MM/dd HH:mm"
|
||||
) else "-"
|
||||
|
||||
val actionsComBoBox = JComboBox<AskTransfer.Action>()
|
||||
actionsComBoBox.addItem(AskTransfer.Action.Overwrite)
|
||||
actionsComBoBox.addItem(AskTransfer.Action.Skip)
|
||||
actionsComBoBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: StringUtils.EMPTY
|
||||
if (value == AskTransfer.Action.Overwrite) {
|
||||
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
||||
} else if (value == AskTransfer.Action.Skip) {
|
||||
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
||||
}
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
val applyAllCheckbox = JCheckBox(I18n.getString("termora.transport.sftp.already-exists.apply-all"))
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(actionsComBoBox)
|
||||
box.add(Box.createHorizontalStrut(8))
|
||||
box.add(applyAllCheckbox)
|
||||
box.add(Box.createHorizontalGlue())
|
||||
|
||||
val ttBox = Box.createVerticalBox()
|
||||
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message1")))
|
||||
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message2")))
|
||||
|
||||
val warningIcon = FlatSVGIcon(
|
||||
Icons.warningIntroduction.name,
|
||||
iconSize,
|
||||
iconSize
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
// tip
|
||||
.add(JLabel(warningIcon)).xy(1, rows, "center, fill")
|
||||
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
||||
// name
|
||||
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
||||
.add(sourceAttr.name).xyw(3, rows, 3).apply { rows += step }
|
||||
// separator
|
||||
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||
// Destination
|
||||
.add("${I18n.getString("termora.transport.sftp.already-exists.destination")}:").xy(1, rows)
|
||||
.apply { rows += step }
|
||||
// Folder
|
||||
.add(JLabel(targetIcon)).xy(1, rows, "center, fill")
|
||||
.add(targetModified).xyw(3, rows, 3).apply { rows += step }
|
||||
// Source
|
||||
.add("${I18n.getString("termora.transport.sftp.already-exists.source")}:").xy(1, rows)
|
||||
.apply { rows += step }
|
||||
// Folder
|
||||
.add(JLabel(sourceIcon)).xy(1, rows, "center, fill")
|
||||
.add(sourceModified).xyw(3, rows, 3).apply { rows += step }
|
||||
// separator
|
||||
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||
// name
|
||||
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.actions")}:")).xy(1, rows)
|
||||
.add(box).xyw(3, rows, 3).apply { rows += step }
|
||||
.build()
|
||||
panel.putClientProperty("SKIP_requestFocusInWindow", true)
|
||||
|
||||
return AskTransfer(
|
||||
option = OptionPane.showConfirmDialog(
|
||||
owner, panel,
|
||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||
title = sourceAttr.name,
|
||||
initialValue = JOptionPane.YES_OPTION,
|
||||
) {
|
||||
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
||||
},
|
||||
action = actionsComBoBox.selectedItem as AskTransfer.Action,
|
||||
applyAll = applyAllCheckbox.isSelected
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.formdev.flatlaf.icons.FlatTreeLeafIcon
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.eclipse.jgit.util.LRUMap
|
||||
import java.util.*
|
||||
@@ -24,7 +25,7 @@ object NativeFileIcons {
|
||||
|
||||
init {
|
||||
if (SystemUtils.IS_OS_UNIX) {
|
||||
cache[SystemUtils.USER_HOME] = Pair(FlatTreeClosedIcon(), I18n.getString("termora.folder"))
|
||||
cache[SystemUtils.USER_HOME] = Pair(folderIcon, I18n.getString("termora.folder"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,35 +37,30 @@ object NativeFileIcons {
|
||||
return getIcon(filename, true).first
|
||||
}
|
||||
|
||||
fun getIcon(filename: String, isFile: Boolean = true): Pair<Icon, String> {
|
||||
if (isFile) {
|
||||
val extension = FilenameUtils.getExtension(filename)
|
||||
if (cache.containsKey(extension)) {
|
||||
return cache.getValue(extension)
|
||||
}
|
||||
} else {
|
||||
if (cache.containsKey(SystemUtils.USER_HOME)) {
|
||||
return cache.getValue(SystemUtils.USER_HOME)
|
||||
}
|
||||
fun getIcon(filename: String, isFile: Boolean = true, width: Int = 16, height: Int = 16): Pair<Icon, String> {
|
||||
val key = if (isFile) FilenameUtils.getExtension(filename) + "." + width + "@" + height
|
||||
else SystemUtils.USER_HOME + "." + width + "@" + height
|
||||
|
||||
if (cache.containsKey(key)) {
|
||||
return cache.getValue(key)
|
||||
}
|
||||
|
||||
val isDirectory = !isFile
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
|
||||
val file = if (isDirectory) FileUtils.getFile(SystemUtils.USER_HOME) else
|
||||
FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}.${filename}")
|
||||
if (isFile && !file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
val icon = getFileSystemView().getSystemIcon(file, 16, 16)
|
||||
|
||||
val icon = getFileSystemView().getSystemIcon(file, width, height) ?: if (isFile) fileIcon else folderIcon
|
||||
val description = getFileSystemView().getSystemTypeDescription(file)
|
||||
?: StringUtils.defaultString(file.extension)
|
||||
val pair = icon to description
|
||||
|
||||
if (isDirectory) {
|
||||
cache[SystemUtils.USER_HOME] = pair
|
||||
} else {
|
||||
cache[FilenameUtils.getExtension(file.name)] = pair
|
||||
}
|
||||
cache[key] = pair
|
||||
|
||||
if (isFile) FileUtils.deleteQuietly(file)
|
||||
|
||||
|
||||
@@ -314,6 +314,16 @@ termora.transport.sftp.status.waiting=Waiting
|
||||
termora.transport.sftp.status.done=Done
|
||||
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.message2=Select a task to do
|
||||
termora.transport.sftp.already-exists.overwrite=Overwrite
|
||||
termora.transport.sftp.already-exists.skip=Skip
|
||||
termora.transport.sftp.already-exists.apply-all=Apply all
|
||||
termora.transport.sftp.already-exists.name=Name
|
||||
termora.transport.sftp.already-exists.destination=Destination
|
||||
termora.transport.sftp.already-exists.source=Source
|
||||
termora.transport.sftp.already-exists.actions=Actions
|
||||
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=Name
|
||||
|
||||
@@ -293,6 +293,19 @@ termora.transport.sftp.status.done=已完成
|
||||
termora.transport.sftp.status.failed=已失败
|
||||
|
||||
|
||||
termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象
|
||||
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
||||
termora.transport.sftp.already-exists.overwrite=覆盖
|
||||
termora.transport.sftp.already-exists.skip=跳过
|
||||
termora.transport.sftp.already-exists.apply-all=应用全部
|
||||
termora.transport.sftp.already-exists.name=名称
|
||||
termora.transport.sftp.already-exists.destination=目标文件
|
||||
termora.transport.sftp.already-exists.source=源文件
|
||||
termora.transport.sftp.already-exists.actions=操作
|
||||
|
||||
|
||||
|
||||
|
||||
# Permission
|
||||
termora.transport.permissions=更改权限
|
||||
termora.transport.permissions.file-folder-permissions=文件/文件夹权限
|
||||
|
||||
@@ -287,6 +287,16 @@ termora.transport.sftp.status.waiting=等待中
|
||||
termora.transport.sftp.status.done=已完成
|
||||
termora.transport.sftp.status.failed=已失敗
|
||||
|
||||
termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象
|
||||
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
||||
termora.transport.sftp.already-exists.overwrite=覆蓋
|
||||
termora.transport.sftp.already-exists.skip=跳過
|
||||
termora.transport.sftp.already-exists.apply-all=應用全部
|
||||
termora.transport.sftp.already-exists.name=名稱
|
||||
termora.transport.sftp.already-exists.destination=目標文件
|
||||
termora.transport.sftp.already-exists.source=原始檔
|
||||
termora.transport.sftp.already-exists.actions=操作
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=名稱
|
||||
termora.transport.jobs.table.status=狀態
|
||||
|
||||
8
src/main/resources/icons/file.svg
Normal file
8
src/main/resources/icons/file.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<rect width="11" height="13" x="2.5" y="1.5" stroke="#6C707E" rx="1.5"/>
|
||||
<line x1="5.5" x2="10.5" y1="5.5" y2="5.5" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<line x1="5.5" x2="10.5" y1="8" y2="8" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<line x1="5.5" x2="10.5" y1="10.5" y2="10.5" stroke="#6C707E" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 497 B |
8
src/main/resources/icons/file_dark.svg
Normal file
8
src/main/resources/icons/file_dark.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<rect width="11" height="13" x="2.5" y="1.5" stroke="#CED0D6" rx="1.5"/>
|
||||
<line x1="5.5" x2="10.5" y1="5.5" y2="5.5" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
<line x1="5.5" x2="10.5" y1="8" y2="8" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
<line x1="5.5" x2="10.5" y1="10.5" y2="10.5" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 497 B |
Reference in New Issue
Block a user