feat: supports importing hosts from CSV (#291)

This commit is contained in:
hstyi
2025-02-22 12:23:31 +08:00
committed by GitHub
parent 1f392c52a1
commit adabaf8f2d
16 changed files with 225 additions and 54 deletions

View File

@@ -38,6 +38,10 @@ commons-text 1.13.0
Apache License 2.0 Apache License 2.0
https://github.com/apache/commons-text/blob/master/LICENSE.txt https://github.com/apache/commons-text/blob/master/LICENSE.txt
commons-csv 1.13.0
Apache License 2.0
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
eddsa 0.3.0 eddsa 0.3.0
Creative Commons Zero v1.0 Universal Creative Commons Zero v1.0 Universal
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt

View File

@@ -59,6 +59,7 @@ dependencies {
implementation(libs.commons.codec) implementation(libs.commons.codec)
implementation(libs.commons.io) implementation(libs.commons.io)
implementation(libs.commons.lang3) implementation(libs.commons.lang3)
implementation(libs.commons.csv)
implementation(libs.commons.net) implementation(libs.commons.net)
implementation(libs.commons.text) implementation(libs.commons.text)
implementation(libs.commons.compress) implementation(libs.commons.compress)

View File

@@ -9,6 +9,7 @@ trove4j = "1.0.20200330"
kotlinx-serialization-json = "1.8.0" kotlinx-serialization-json = "1.8.0"
commons-codec = "1.18.0" commons-codec = "1.18.0"
commons-lang3 = "3.17.0" commons-lang3 = "3.17.0"
commons-csv = "1.13.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"
@@ -53,6 +54,7 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" } commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" } commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" } commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
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" }
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" } pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }

View File

@@ -17,7 +17,11 @@ class HostManager private constructor() {
fun addHost(host: Host) { fun addHost(host: Host) {
assertEventDispatchThread() assertEventDispatchThread()
database.addHost(host) database.addHost(host)
setHost(host) if (host.deleted) {
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
} else {
hosts[host.id] = host
}
} }
/** /**
@@ -39,12 +43,4 @@ class HostManager private constructor() {
return hosts[id] return hosts[id]
} }
/**
* 仅修改缓存中的
*/
fun setHost(host: Host) {
assertEventDispatchThread()
hosts[host.id] = host
}
} }

View File

@@ -1,18 +1,26 @@
package app.termora package app.termora
import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeNode
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) { class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
companion object { companion object {
private val hostManager get() = HostManager.getInstance() private val hostManager get() = HostManager.getInstance()
} }
/**
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
*/
var host: Host var host: Host
get() = hostManager.getHost((userObject as Host).id) ?: userObject as Host get() {
set(value) { val cacheHost = hostManager.getHost((userObject as Host).id)
setUserObject(value) val myHost = userObject as Host
hostManager.setHost(value) if (cacheHost == null) {
return myHost
} }
return if (cacheHost.updateDate > myHost.updateDate) cacheHost else myHost
}
set(value) = setUserObject(value)
val folderCount val folderCount
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false } get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
@@ -32,6 +40,29 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
return children return children
} }
fun childrenNode(): List<HostTreeNode> {
return children?.map { it as HostTreeNode } ?: emptyList()
}
/**
* 深度克隆
* @param scopes 克隆的范围
*/
fun clone(scopes: Set<Protocol> = emptySet()): HostTreeNode {
val newNode = clone() as HostTreeNode
deepClone(newNode, this, scopes)
return newNode
}
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
for (child in oldNode.childrenNode()) {
if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue
val newChildNode = child.clone() as HostTreeNode
deepClone(newChildNode, child, scopes)
newNode.add(newChildNode)
}
}
override fun clone(): Any { override fun clone(): Any {
val newNode = HostTreeNode(host) val newNode = HostTreeNode(host)
@@ -40,6 +71,17 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
return newNode return newNode
} }
override fun isNodeChild(aNode: TreeNode?): Boolean {
if (aNode is HostTreeNode) {
for (node in childrenNode()) {
if (node.host == aNode.host) {
return true
}
}
}
return super.isNodeChild(aNode)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -11,12 +11,16 @@ import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVPrinter
import org.apache.commons.io.FileUtils 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.jdesktop.swingx.JXTree import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
import org.slf4j.LoggerFactory
import java.awt.Component import java.awt.Component
import java.awt.Dimension import java.awt.Dimension
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
@@ -26,7 +30,7 @@ import java.awt.event.ActionEvent
import java.awt.event.ActionListener import java.awt.event.ActionListener
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.io.File import java.io.*
import java.util.* import java.util.*
import java.util.function.Function import java.util.function.Function
import javax.swing.* import javax.swing.*
@@ -41,6 +45,11 @@ import kotlin.math.min
class NewHostTree : JXTree() { class NewHostTree : JXTree() {
companion object {
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
}
private val tree = this private val tree = this
private val editor = OutlineTextField(64) private val editor = OutlineTextField(64)
private val hostManager get() = HostManager.getInstance() private val hostManager get() = HostManager.getInstance()
@@ -207,7 +216,7 @@ class NewHostTree : JXTree() {
if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) { if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) {
return return
} }
lastHost.host = lastHost.host.copy(name = editor.text) lastHost.host = lastHost.host.copy(name = editor.text, updateDate = System.currentTimeMillis())
hostManager.addHost(lastHost.host) hostManager.addHost(lastHost.host)
} }
@@ -298,7 +307,7 @@ class NewHostTree : JXTree() {
// 转移 // 转移
for (e in nodes) { for (e in nodes) {
model.removeNodeFromParent(e) model.removeNodeFromParent(e)
e.host = e.host.copy(parentId = node.host.id) e.host = e.host.copy(parentId = node.host.id, updateDate = System.currentTimeMillis())
hostManager.addHost(e.host) hostManager.addHost(e.host)
if (dropLocation.childIndex == -1) { if (dropLocation.childIndex == -1) {
@@ -347,6 +356,7 @@ class NewHostTree : JXTree() {
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder")) val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host")) val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import")) val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import"))
val csvMenu = importMenu.add("CSV")
val windTermMenu = importMenu.add("WindTerm") val windTermMenu = importMenu.add("WindTerm")
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect")) val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
@@ -375,6 +385,9 @@ class NewHostTree : JXTree() {
popupMenu.add(showMoreInfo) popupMenu.add(showMoreInfo)
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property")) val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
csvMenu.addActionListener {
importHosts(lastNode, ImportType.CSV)
}
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) } windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
open.addActionListener { openHosts(it, false) } open.addActionListener { openHosts(it, false) }
openInNewWindow.addActionListener { openHosts(it, true) } openInNewWindow.addActionListener { openHosts(it, true) }
@@ -604,6 +617,17 @@ class NewHostTree : JXTree() {
} }
private fun importHosts(folder: HostTreeNode, type: ImportType) { private fun importHosts(folder: HostTreeNode, type: ImportType) {
try {
doImportHosts(folder, type)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(e), messageType = JOptionPane.ERROR_MESSAGE)
}
}
private fun doImportHosts(folder: HostTreeNode, type: ImportType) {
val chooser = JFileChooser() val chooser = JFileChooser()
chooser.fileSelectionMode = JFileChooser.FILES_ONLY chooser.fileSelectionMode = JFileChooser.FILES_ONLY
chooser.isAcceptAllFileFilterUsed = false chooser.isAcceptAllFileFilterUsed = false
@@ -611,6 +635,8 @@ class NewHostTree : JXTree() {
if (type == ImportType.WindTerm) { if (type == ImportType.WindTerm) {
chooser.fileFilter = FileNameExtensionFilter("WindTerm(*.sessions)", "sessions") chooser.fileFilter = FileNameExtensionFilter("WindTerm(*.sessions)", "sessions")
} else if (type == ImportType.CSV) {
chooser.fileFilter = FileNameExtensionFilter("CSV(*.csv)", "csv")
} }
val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY) val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY)
@@ -621,7 +647,47 @@ class NewHostTree : JXTree() {
} }
} }
// csv template
if (type == ImportType.CSV) {
val code = OptionPane.showConfirmDialog(
owner,
I18n.getString("termora.welcome.contextmenu.import.csv.download-template"),
optionType = JOptionPane.YES_NO_OPTION,
messageType = JOptionPane.QUESTION_MESSAGE,
options = arrayOf(
I18n.getString("termora.welcome.contextmenu.import"),
I18n.getString("termora.welcome.contextmenu.download")
),
initialValue = I18n.getString("termora.welcome.contextmenu.import")
)
if (code == JOptionPane.DEFAULT_OPTION) {
return
} else if (code != JOptionPane.YES_OPTION) {
chooser.setSelectedFile(File("termora_import.csv"))
if (chooser.showSaveDialog(owner) == JFileChooser.APPROVE_OPTION) {
CSVPrinter(
FileWriter(chooser.selectedFile, Charsets.UTF_8),
CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()
).use { printer ->
printer.printRecord("Projects/Dev", "Web Server", "192.168.1.1", "22", "root", "SSH")
printer.printRecord("Projects/Prod", "Web Server", "serverhost.com", "2222", "root", "SSH")
printer.printRecord(StringUtils.EMPTY, "Web Server", "serverhost.com", "2222", "user", "SSH")
}
OptionPane.openFileInFolder(
owner,
chooser.selectedFile,
I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done-open-folder"),
I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done")
)
}
return
}
}
// 选择文件
val code = chooser.showOpenDialog(owner) val code = chooser.showOpenDialog(owner)
// 记住目录
properties.putString("NewHostTree.ImportHosts.defaultDir", chooser.currentDirectory.absolutePath) properties.putString("NewHostTree.ImportHosts.defaultDir", chooser.currentDirectory.absolutePath)
if (code != JFileChooser.APPROVE_OPTION) { if (code != JFileChooser.APPROVE_OPTION) {
@@ -630,15 +696,16 @@ class NewHostTree : JXTree() {
val file = chooser.selectedFile val file = chooser.selectedFile
val nodes = if (type == ImportType.WindTerm) { val nodes = when (type) {
parseFromWindTerm(file) ImportType.WindTerm -> parseFromWindTerm(folder, file)
} else { ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) }
emptyList()
} }
for (node in nodes) { for (node in nodes) {
node.host = node.host.copy(parentId = folder.host.id) node.host = node.host.copy(parentId = folder.host.id, updateDate = System.currentTimeMillis())
if (folder.getIndex(node) != -1) {
continue
}
model.insertNodeInto( model.insertNodeInto(
node, node,
folder, folder,
@@ -650,36 +717,73 @@ class NewHostTree : JXTree() {
hostManager.addHost(node.host) hostManager.addHost(node.host)
node.getAllChildren().forEach { hostManager.addHost(it.host) } node.getAllChildren().forEach { hostManager.addHost(it.host) }
} }
// 重新加载
model.reload(folder)
} }
private fun parseFromWindTerm(file: File): List<HostTreeNode> { private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
val sessions = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()).jsonArray } val sessions = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()).jsonArray }
.onFailure { OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(it)) } .onFailure { OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(it)) }
.getOrNull() ?: return emptyList() .getOrNull() ?: return emptyList()
val nodes = mutableListOf<HostTreeNode>()
val sw = StringWriter()
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
for (i in 0 until sessions.size) { for (i in 0 until sessions.size) {
val json = sessions[i].jsonObject val json = sessions[i].jsonObject
val protocol = json["session.protocol"]?.jsonPrimitive?.content ?: StringUtils.EMPTY val protocol = json["session.protocol"]?.jsonPrimitive?.content ?: "SSH"
if (protocol != "SSH") continue if (!StringUtils.equalsIgnoreCase("SSH", protocol)) continue
val label = json["session.label"]?.jsonPrimitive?.content ?: StringUtils.EMPTY val label = json["session.label"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val target = json["session.target"]?.jsonPrimitive?.content ?: StringUtils.EMPTY val target = json["session.target"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val port = json["session.port"]?.jsonPrimitive?.intOrNull ?: 22 val port = json["session.port"]?.jsonPrimitive?.intOrNull ?: 22
val group = json["session.group"]?.jsonPrimitive?.content ?: StringUtils.EMPTY val group = json["session.group"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val groups = group.split(">") val groups = group.split(">")
printer.printRecord(groups.joinToString("/"), label, target, port, StringUtils.EMPTY, "SSH")
}
}
return parseFromCSV(folder, StringReader(sw.toString()))
}
private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> {
val records = CSVParser.builder()
.setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get())
.setCharset(Charsets.UTF_8)
.setReader(sr)
.get()
.use { it.records }
// 把现有目录提取出来,避免重复创建
val nodes = folderNode.clone(setOf(Protocol.Folder))
.childrenNode().filter { it.host.protocol == Protocol.Folder }
.toMutableList()
for (record in records) {
val map = mutableMapOf<String, String>()
for (e in record.parser.headerMap.keys) {
map[e] = record.get(e)
}
val folder = map["Folders"] ?: StringUtils.EMPTY
val label = map["Label"] ?: StringUtils.EMPTY
val hostname = map["Hostname"] ?: StringUtils.EMPTY
val port = map["Port"]?.toIntOrNull() ?: 22
val username = map["Username"] ?: StringUtils.EMPTY
val protocol = map["Protocol"] ?: "SSH"
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
if (StringUtils.isAllBlank(hostname, label)) continue
var p: HostTreeNode? = null var p: HostTreeNode? = null
if (group.isNotBlank()) { if (folder.isNotBlank()) {
for (j in groups.indices) { for ((j, name) in folder.split("/").withIndex()) {
val folders = if (j == 0 || p == null) nodes val folders = if (j == 0 || p == null) nodes
else p.children().toList().filterIsInstance<HostTreeNode>() else p.children().toList().filterIsInstance<HostTreeNode>()
val n = HostTreeNode( val n = HostTreeNode(
Host( Host(
name = groups[j], protocol = Protocol.Folder, name = name, protocol = Protocol.Folder,
parentId = p?.host?.id ?: StringUtils.EMPTY parentId = p?.host?.id ?: StringUtils.EMPTY
) )
) )
val cp = folders.find { it.host.protocol == Protocol.Folder && it.host.name == groups[j] } val cp = folders.find { it.host.protocol == Protocol.Folder && it.host.name == name }
if (cp != null) { if (cp != null) {
p = cp p = cp
continue continue
@@ -696,9 +800,10 @@ class NewHostTree : JXTree() {
val n = HostTreeNode( val n = HostTreeNode(
Host( Host(
name = StringUtils.defaultIfBlank(label, target), name = StringUtils.defaultIfBlank(label, hostname),
host = target, host = hostname,
port = port, port = port,
username = username,
protocol = Protocol.SSH, protocol = Protocol.SSH,
parentId = p?.host?.id ?: StringUtils.EMPTY, parentId = p?.host?.id ?: StringUtils.EMPTY,
) )
@@ -716,7 +821,8 @@ class NewHostTree : JXTree() {
private enum class ImportType { private enum class ImportType {
WindTerm WindTerm,
CSV,
} }
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable { private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {

View File

@@ -73,7 +73,7 @@ class NewHostTreeModel : DefaultTreeModel(
for ((i, c) in parent.children().toList().filterIsInstance<HostTreeNode>().withIndex()) { for ((i, c) in parent.children().toList().filterIsInstance<HostTreeNode>().withIndex()) {
val sort = i.toLong() val sort = i.toLong()
if (c.host.sort == sort) continue if (c.host.sort == sort) continue
c.host = c.host.copy(sort = sort) c.host = c.host.copy(sort = sort, updateDate = System.currentTimeMillis())
hostManager.addHost(c.host) hostManager.addHost(c.host)
} }
} }

View File

@@ -50,6 +50,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
// 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地 // 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地
if (useJumpHosts) { if (useJumpHosts) {
host = host.copy( host = host.copy(
updateDate = System.currentTimeMillis(),
tunnelings = listOf( tunnelings = listOf(
Tunneling( Tunneling(
type = TunnelingType.Local, type = TunnelingType.Local,
@@ -66,7 +67,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
// 打开通道 // 打开通道
for (tunneling in host.tunnelings) { for (tunneling in host.tunnelings) {
val address = SshClients.openTunneling(sshSession, host, tunneling) val address = SshClients.openTunneling(sshSession, host, tunneling)
host = host.copy(host = address.hostName, port = address.port) host = host.copy(
host = address.hostName,
port = address.port,
updateDate = System.currentTimeMillis(),
)
} }
} }
@@ -128,9 +133,9 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
private fun setAuthentication(commands: MutableList<String>, host: Host) { private fun setAuthentication(commands: MutableList<String>, host: Host) {
// 如果通过公钥连接 // 如果通过公钥连接
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.PublicKey) {
val keyPair = keyManager.getOhKeyPair(host.authentication.password) val ohKeyPair = keyManager.getOhKeyPair(host.authentication.password)
if (keyPair != null) { if (ohKeyPair != null) {
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(keyPair) val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair)
val privateKeyPath = Application.createSubTemporaryDir() val privateKeyPath = Application.createSubTemporaryDir()
val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY) val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY)
Files.newOutputStream(privateKeyFile) Files.newOutputStream(privateKeyFile)

View File

@@ -89,7 +89,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
terminal.write("SSH client is opening...\r\n") terminal.write("SSH client is opening...\r\n")
} }
var host = this.host.copy(authentication = this.host.authentication.copy()) var host =
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
val owner = SwingUtilities.getWindowAncestor(terminalPanel) val owner = SwingUtilities.getWindowAncestor(terminalPanel)
val client = SshClients.openClient(host).also { sshClient = it } val client = SshClients.openClient(host).also { sshClient = it }
client.serverKeyVerifier = DialogServerKeyVerifier(owner) client.serverKeyVerifier = DialogServerKeyVerifier(owner)
@@ -103,13 +104,14 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
host = host.copy( host = host.copy(
authentication = authentication, authentication = authentication,
username = dialog.getUsername(), username = dialog.getUsername(),
updateDate = System.currentTimeMillis(),
) )
// save // save
if (dialog.isRemembered()) { if (dialog.isRemembered()) {
HostManager.getInstance().addHost( HostManager.getInstance().addHost(
tab.host.copy( tab.host.copy(
authentication = authentication, authentication = authentication,
username = dialog.getUsername(), username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
) )
) )
} }

View File

@@ -151,7 +151,7 @@ object SshClients {
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}") log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
} }
// 映射完毕之后修改Host和端口 // 映射完毕之后修改Host和端口
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port) jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
} }
} }

View File

@@ -356,7 +356,7 @@ class TerminalTabbed(
} }
host = host.copy( host = host.copy(
protocol = Protocol.SFTPPty, protocol = Protocol.SFTPPty, updateDate = System.currentTimeMillis(),
options = host.options.copy(env = envs.toPropertiesString()) options = host.options.copy(env = envs.toPropertiesString())
) )
} }

View File

@@ -14,7 +14,7 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
val tab = openOrCreateSFTPTerminalTab(evt) ?: return val tab = openOrCreateSFTPTerminalTab(evt) ?: return
if (host != null) { if (host != null) {
connectHost(host.copy(protocol = Protocol.SSH), tab) connectHost(host.copy(protocol = Protocol.SSH, updateDate = System.currentTimeMillis()), tab)
} }
} }

View File

@@ -107,7 +107,7 @@ class SftpFileSystemPanel(
private suspend fun doConnect() { private suspend fun doConnect() {
val thisHost = this.host ?: return val thisHost = this.host ?: return
var host = thisHost.copy(authentication = thisHost.authentication.copy()) var host = thisHost.copy(authentication = thisHost.authentication.copy(), updateDate = System.currentTimeMillis())
closeIO() closeIO()
@@ -123,14 +123,14 @@ class SftpFileSystemPanel(
val authentication = dialog.getAuthentication() val authentication = dialog.getAuthentication()
host = host.copy( host = host.copy(
authentication = authentication, authentication = authentication,
username = dialog.getUsername(), username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
) )
// save // save
if (dialog.isRemembered()) { if (dialog.isRemembered()) {
HostManager.getInstance().addHost( HostManager.getInstance().addHost(
host.copy( host.copy(
authentication = authentication, authentication = authentication,
username = dialog.getUsername(), username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
) )
) )
} }

View File

@@ -144,6 +144,10 @@ termora.welcome.contextmenu.new.host=Host
termora.welcome.contextmenu.new.folder.name=New Folder termora.welcome.contextmenu.new.folder.name=New Folder
termora.welcome.contextmenu.property=Properties termora.welcome.contextmenu.property=Properties
termora.welcome.contextmenu.show-more-info=Show more info termora.welcome.contextmenu.show-more-info=Show more info
termora.welcome.contextmenu.download=Download
termora.welcome.contextmenu.import.csv.download-template=Do you want to import or download the template?
termora.welcome.contextmenu.import.csv.download-template-done=Download the template successfully
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=Download the template successfully, Do you want to open the folder?
# New Host # New Host
termora.new-host.title=Create a new host termora.new-host.title=Create a new host

View File

@@ -132,6 +132,11 @@ termora.welcome.contextmenu.new.folder.name=新建文件夹
termora.welcome.contextmenu.property=属性 termora.welcome.contextmenu.property=属性
termora.welcome.contextmenu.show-more-info=显示更多信息 termora.welcome.contextmenu.show-more-info=显示更多信息
termora.welcome.contextmenu.download=下载
termora.welcome.contextmenu.import.csv.download-template=您要导入还是下载模板?
termora.welcome.contextmenu.import.csv.download-template-done=下载成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹?
# New Host # New Host
termora.new-host.title=新建主机 termora.new-host.title=新建主机
termora.new-host.general=属性 termora.new-host.general=属性

View File

@@ -130,6 +130,10 @@ termora.welcome.contextmenu.new.host=主機
termora.welcome.contextmenu.new.folder.name=新建資料夾 termora.welcome.contextmenu.new.folder.name=新建資料夾
termora.welcome.contextmenu.property=屬性 termora.welcome.contextmenu.property=屬性
termora.welcome.contextmenu.show-more-info=顯示更多信息 termora.welcome.contextmenu.show-more-info=顯示更多信息
termora.welcome.contextmenu.download=下載
termora.welcome.contextmenu.import.csv.download-template=您要匯入還是下載範本?
termora.welcome.contextmenu.import.csv.download-template-done=下載成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾?
# New Host # New Host
termora.new-host.title=新主機 termora.new-host.title=新主機