From 022ee20eed317d7c0a9cd50f77153e3821a426ff Mon Sep 17 00:00:00 2001 From: Lykin <137850705+tiny-craft@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:39:34 +0800 Subject: [PATCH] feat: support import/export connection profiles --- backend/services/connection_service.go | 117 ++++++++++++++++++ .../src/components/sidebar/ConnectionPane.vue | 37 +++++- frontend/src/langs/en-us.json | 2 + frontend/src/langs/zh-cn.json | 2 + frontend/src/stores/connections.js | 31 +++++ 5 files changed, 187 insertions(+), 2 deletions(-) diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index 4f5727d..e20f267 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -6,10 +6,15 @@ import ( "crypto/x509" "errors" "fmt" + "github.com/klauspost/compress/zip" "github.com/redis/go-redis/v9" + "github.com/vrischmann/userdir" + "github.com/wailsapp/wails/v2/pkg/runtime" "golang.org/x/crypto/ssh" + "io" "net" "os" + "path" "strings" "sync" "time" @@ -395,3 +400,115 @@ func (c *connectionService) SaveRefreshInterval(name string, interval int) (resp resp.Success = true return } + +// ExportConnections export connections to zip file +func (c *connectionService) ExportConnections() (resp types.JSResp) { + defaultFileName := "connections_" + time.Now().Format("20060102150405") + ".zip" + filepath, err := runtime.SaveFileDialog(c.ctx, runtime.SaveDialogOptions{ + ShowHiddenFiles: true, + DefaultFilename: defaultFileName, + Filters: []runtime.FileFilter{ + { + Pattern: "*.zip", + }, + }, + }) + if err != nil { + resp.Msg = err.Error() + return + } + + // compress the connections profile with zip + const connectionFilename = "connections.yaml" + inputFile, err := os.Open(path.Join(userdir.GetConfigHome(), "TinyRDM", connectionFilename)) + if err != nil { + resp.Msg = err.Error() + return + } + defer inputFile.Close() + + outputFile, err := os.Create(filepath) + if err != nil { + resp.Msg = err.Error() + return + } + defer outputFile.Close() + + zipWriter := zip.NewWriter(outputFile) + defer zipWriter.Close() + + headerWriter, err := zipWriter.CreateHeader(&zip.FileHeader{ + Name: connectionFilename, + Method: zip.Deflate, + }) + if err != nil { + resp.Msg = err.Error() + return + } + + if _, err = io.Copy(headerWriter, inputFile); err != nil { + resp.Msg = err.Error() + return + } + + resp.Success = true + resp.Data = struct { + Path string `json:"path"` + }{ + Path: filepath, + } + return +} + +// ImportConnections import connections from local zip file +func (c *connectionService) ImportConnections() (resp types.JSResp) { + filepath, err := runtime.OpenFileDialog(c.ctx, runtime.OpenDialogOptions{ + ShowHiddenFiles: true, + Filters: []runtime.FileFilter{ + { + Pattern: "*.zip", + }, + }, + }) + if err != nil { + resp.Msg = err.Error() + return + } + + const connectionFilename = "connections.yaml" + zipFile, err := zip.OpenReader(filepath) + if err != nil { + resp.Msg = err.Error() + return + } + + var file *zip.File + for _, file = range zipFile.File { + if file.Name == connectionFilename { + break + } + } + if file != nil { + zippedFile, err := file.Open() + if err != nil { + resp.Msg = err.Error() + return + } + defer zippedFile.Close() + + outputFile, err := os.Create(path.Join(userdir.GetConfigHome(), "TinyRDM", connectionFilename)) + if err != nil { + resp.Msg = err.Error() + return + } + defer outputFile.Close() + + if _, err = io.Copy(outputFile, zippedFile); err != nil { + resp.Msg = err.Error() + return + } + } + + resp.Success = true + return +} diff --git a/frontend/src/components/sidebar/ConnectionPane.vue b/frontend/src/components/sidebar/ConnectionPane.vue index 03fce80..969e0b0 100644 --- a/frontend/src/components/sidebar/ConnectionPane.vue +++ b/frontend/src/components/sidebar/ConnectionPane.vue @@ -7,10 +7,34 @@ import IconButton from '@/components/common/IconButton.vue' import Filter from '@/components/icons/Filter.vue' import ConnectionTree from './ConnectionTree.vue' import { ref } from 'vue' +import More from '@/components/icons/More.vue' +import Import from '@/components/icons/Import.vue' +import { useRender } from '@/utils/render.js' +import Export from '@/components/icons/Export.vue' +import useConnectionStore from 'stores/connections.js' const themeVars = useThemeVars() const dialogStore = useDialogStore() +const connectionStore = useConnectionStore() +const render = useRender() const filterPattern = ref('') + +const moreOptions = [ + { key: 'import', label: 'interface.import_conn', icon: Import }, + { key: 'export', label: 'interface.export_conn', icon: Export }, +] + +const onSelectOptions = async (select) => { + switch (select) { + case 'import': + await connectionStore.importConnections() + await connectionStore.initConnections(true) + break + case 'export': + await connectionStore.exportConnections() + break + } +} + + + diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json index 56f90fa..5e1a443 100644 --- a/frontend/src/langs/en-us.json +++ b/frontend/src/langs/en-us.json @@ -74,6 +74,8 @@ "edit_conn_group": "Edit Group", "rename_conn_group": "Rename Group", "remove_conn_group": "Delete Group", + "import_conn": "Import Connections...", + "export_conn": "Export Connections...", "ttl": "TTL", "forever": "Forever", "rename_key": "Rename Key", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 8a7a774..0d990b1 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -74,6 +74,8 @@ "edit_conn_group": "编辑分组", "rename_conn_group": "重命名分组", "remove_conn_group": "删除分组", + "import_conn": "导入连接...", + "export_conn": "导出连接...", "ttl": "TTL", "forever": "永久", "rename_key": "重命名键", diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index d0dd572..2cd9540 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -4,7 +4,9 @@ import { CreateGroup, DeleteConnection, DeleteGroup, + ExportConnections, GetConnection, + ImportConnections, ListConnection, RenameGroup, SaveConnection, @@ -15,6 +17,7 @@ import { import { ConnectionType } from '@/consts/connection_type.js' import { KeyViewType } from '@/consts/key_view_type.js' import useBrowserStore from 'stores/browser.js' +import { i18nGlobal } from '@/utils/i18n.js' const useConnectionStore = defineStore('connections', { /** @@ -399,6 +402,34 @@ const useConnectionStore = defineStore('connections', { } return { success: true } }, + + async exportConnections() { + const { + success, + msg, + data: { path = '' }, + } = await ExportConnections() + if (!success) { + if (!isEmpty(msg)) { + $message.error(msg) + return + } + } + + $message.success(i18nGlobal.t('dialogue.handle_succ')) + }, + + async importConnections() { + const { success, msg } = await ImportConnections() + if (!success) { + if (!isEmpty(msg)) { + $message.error(msg) + return + } + } + + $message.success(i18nGlobal.t('dialogue.handle_succ')) + }, }, })