mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: supports importing hosts from CSV (#291)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=属性
|
||||||
|
|||||||
@@ -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=新主機
|
||||||
|
|||||||
Reference in New Issue
Block a user