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
+ }
+}
@@ -23,14 +47,14 @@ const filterPattern = ref('')
:button-class="['nav-pane-func-btn']"
:icon="AddLink"
size="20"
- stroke-width="4"
+ :stroke-width="3.5"
t-tooltip="interface.new_conn"
@click="dialogStore.openNewDialog()" />
@@ -39,6 +63,15 @@ const filterPattern = ref('')
+
+
+
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'))
+ },
},
})