feat: supports importing hosts from FinalShell (#295)

This commit is contained in:
hstyi
2025-02-22 15:32:48 +08:00
committed by GitHub
parent 0552917c26
commit 05fe6a0eb1
4 changed files with 57 additions and 4 deletions

View File

@@ -16,6 +16,7 @@ import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVPrinter import org.apache.commons.csv.CSVPrinter
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.filefilter.FileFilterUtils
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.ini4j.Ini import org.ini4j.Ini
@@ -365,6 +366,7 @@ class NewHostTree : JXTree() {
val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import")) val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import"))
val csvMenu = importMenu.add("CSV") val csvMenu = importMenu.add("CSV")
val xShellMenu = importMenu.add("Xshell") val xShellMenu = importMenu.add("Xshell")
val finalShellMenu = importMenu.add("FinalShell")
val windTermMenu = importMenu.add("WindTerm") val windTermMenu = importMenu.add("WindTerm")
val secureCRTMenu = importMenu.add("SecureCRT") val secureCRTMenu = importMenu.add("SecureCRT")
val mobaXtermMenu = importMenu.add("MobaXterm") val mobaXtermMenu = importMenu.add("MobaXterm")
@@ -398,6 +400,7 @@ class NewHostTree : JXTree() {
xShellMenu.addActionListener { importHosts(lastNode, ImportType.Xshell) } xShellMenu.addActionListener { importHosts(lastNode, ImportType.Xshell) }
secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) } secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) }
mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) } mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) }
finalShellMenu.addActionListener { importHosts(lastNode, ImportType.FinalShell) }
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) } 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) }
@@ -656,6 +659,11 @@ class NewHostTree : JXTree() {
chooser.dialogTitle = "Xshell Sessions" chooser.dialogTitle = "Xshell Sessions"
chooser.isAcceptAllFileFilterUsed = true chooser.isAcceptAllFileFilterUsed = true
} }
ImportType.FinalShell -> {
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
chooser.isAcceptAllFileFilterUsed = true
}
} }
val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY) val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY)
@@ -706,20 +714,22 @@ class NewHostTree : JXTree() {
// 选择文件 // 选择文件
val code = chooser.showOpenDialog(owner) val code = chooser.showOpenDialog(owner)
// 记住目录
properties.putString("NewHostTree.ImportHosts.defaultDir", chooser.currentDirectory.absolutePath)
if (code != JFileChooser.APPROVE_OPTION) { if (code != JFileChooser.APPROVE_OPTION) {
return return
} }
val file = chooser.selectedFile val file = chooser.selectedFile
properties.putString(
"NewHostTree.ImportHosts.defaultDir",
(if (FileUtils.isDirectory(file)) file else file.parentFile).absolutePath
)
val nodes = when (type) { val nodes = when (type) {
ImportType.WindTerm -> parseFromWindTerm(folder, file) ImportType.WindTerm -> parseFromWindTerm(folder, file)
ImportType.SecureCRT -> parseFromSecureCRT(folder, file) ImportType.SecureCRT -> parseFromSecureCRT(folder, file)
ImportType.MobaXterm -> parseFromMobaXterm(folder, file) ImportType.MobaXterm -> parseFromMobaXterm(folder, file)
ImportType.Xshell -> parseFromXshell(folder, file) ImportType.Xshell -> parseFromXshell(folder, file)
ImportType.FinalShell -> parseFromFinalShell(folder, file)
ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) } ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) }
} }
@@ -874,6 +884,45 @@ class NewHostTree : JXTree() {
return parseFromCSV(folder, StringReader(sw.toString())) return parseFromCSV(folder, StringReader(sw.toString()))
} }
private fun parseFromFinalShell(folder: HostTreeNode, dir: File): List<HostTreeNode> {
val files = FileUtils.listFiles(
dir,
FileFilterUtils.suffixFileFilter("_connect_config.json"),
FileFilterUtils.trueFileFilter()
)
if (files.isEmpty()) {
OptionPane.showMessageDialog(
owner,
I18n.getString("termora.welcome.contextmenu.import.finalshell-folder-empty")
)
return emptyList()
}
val sw = StringWriter()
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
for (file in files) {
try {
val json = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()) }
.getOrNull()?.jsonObject ?: continue
val username = json["user_name"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val label = json["name"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val host = json["host"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
val port = json["port"]?.jsonPrimitive?.intOrNull ?: 22
if (StringUtils.isAllBlank(host, label)) continue
val folders = FilenameUtils.separatorsToUnix(file.parentFile.relativeTo(dir).toString())
printer.printRecord(folders, StringUtils.defaultIfBlank(label, host), host, port, username, "SSH")
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(file.absolutePath, e)
}
}
}
}
return parseFromCSV(folder, StringReader(sw.toString()))
}
private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> { private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> {
val records = CSVParser.builder() val records = CSVParser.builder()
.setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get()) .setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get())
@@ -954,7 +1003,8 @@ class NewHostTree : JXTree() {
CSV, CSV,
Xshell, Xshell,
SecureCRT, SecureCRT,
MobaXterm MobaXterm,
FinalShell,
} }
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable { private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {

View File

@@ -149,6 +149,7 @@ termora.welcome.contextmenu.import.csv.download-template=Do you want to import o
termora.welcome.contextmenu.import.csv.download-template-done=Download the template successfully 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? termora.welcome.contextmenu.import.csv.download-template-done-open-folder=Download the template successfully, Do you want to open the folder?
termora.welcome.contextmenu.import.xshell-folder-empty=The folder does not contain any *.xsh files, Please select the correct Xshell Sessions directory termora.welcome.contextmenu.import.xshell-folder-empty=The folder does not contain any *.xsh files, Please select the correct Xshell Sessions directory
termora.welcome.contextmenu.import.finalshell-folder-empty=The folder does not contain any *_connect_config.json files, Please select the correct FinalShell directory
# New Host # New Host
termora.new-host.title=Create a new host termora.new-host.title=Create a new host

View File

@@ -137,6 +137,7 @@ termora.welcome.contextmenu.import.csv.download-template=您要导入还是下
termora.welcome.contextmenu.import.csv.download-template-done=下载成功 termora.welcome.contextmenu.import.csv.download-template-done=下载成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹? termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹?
termora.welcome.contextmenu.import.xshell-folder-empty=该文件夹不包含 *.xsh 文件,请选择正确的 Xshell 会话目录 termora.welcome.contextmenu.import.xshell-folder-empty=该文件夹不包含 *.xsh 文件,请选择正确的 Xshell 会话目录
termora.welcome.contextmenu.import.finalshell-folder-empty=该文件夹不包含 *_connect_config.json 文件,请选择正确的 FinalShell 配置目录
# New Host # New Host
termora.new-host.title=新建主机 termora.new-host.title=新建主机

View File

@@ -135,6 +135,7 @@ termora.welcome.contextmenu.import.csv.download-template=您要匯入還是下
termora.welcome.contextmenu.import.csv.download-template-done=下載成功 termora.welcome.contextmenu.import.csv.download-template-done=下載成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾? termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾?
termora.welcome.contextmenu.import.xshell-folder-empty=該資料夾不包含 *.xsh 文件,請選擇正確的 Xshell 會話目錄 termora.welcome.contextmenu.import.xshell-folder-empty=該資料夾不包含 *.xsh 文件,請選擇正確的 Xshell 會話目錄
termora.welcome.contextmenu.import.finalshell-folder-empty=該資料夾不包含 *_connect_config.json 文件,請選擇正確的 FinalShell 設定目錄
# New Host # New Host
termora.new-host.title=新主機 termora.new-host.title=新主機