feat: support import/export connection profiles

This commit is contained in:
Lykin 2024-01-18 14:39:34 +08:00
parent 9402af2433
commit 022ee20eed
5 changed files with 187 additions and 2 deletions

View File

@ -6,10 +6,15 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"github.com/klauspost/compress/zip"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/vrischmann/userdir"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"io"
"net" "net"
"os" "os"
"path"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -395,3 +400,115 @@ func (c *connectionService) SaveRefreshInterval(name string, interval int) (resp
resp.Success = true resp.Success = true
return 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
}

View File

@ -7,10 +7,34 @@ import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue' import Filter from '@/components/icons/Filter.vue'
import ConnectionTree from './ConnectionTree.vue' import ConnectionTree from './ConnectionTree.vue'
import { ref } from '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 themeVars = useThemeVars()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const connectionStore = useConnectionStore()
const render = useRender()
const filterPattern = ref('') 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
}
}
</script> </script>
<template> <template>
@ -23,14 +47,14 @@ const filterPattern = ref('')
:button-class="['nav-pane-func-btn']" :button-class="['nav-pane-func-btn']"
:icon="AddLink" :icon="AddLink"
size="20" size="20"
stroke-width="4" :stroke-width="3.5"
t-tooltip="interface.new_conn" t-tooltip="interface.new_conn"
@click="dialogStore.openNewDialog()" /> @click="dialogStore.openNewDialog()" />
<icon-button <icon-button
:button-class="['nav-pane-func-btn']" :button-class="['nav-pane-func-btn']"
:icon="AddGroup" :icon="AddGroup"
size="20" size="20"
stroke-width="4" :stroke-width="3.5"
t-tooltip="interface.new_group" t-tooltip="interface.new_group"
@click="dialogStore.openNewGroupDialog()" /> @click="dialogStore.openNewGroupDialog()" />
<n-divider vertical /> <n-divider vertical />
@ -39,6 +63,15 @@ const filterPattern = ref('')
<n-icon :component="Filter" size="20" /> <n-icon :component="Filter" size="20" />
</template> </template>
</n-input> </n-input>
<n-dropdown
:options="moreOptions"
:render-icon="({ icon }) => render.renderIcon(icon, { strokeWidth: 3.5 })"
:render-label="({ label }) => $t(label)"
placement="top-end"
style="min-width: 130px"
@select="onSelectOptions">
<icon-button :button-class="['nav-pane-func-btn']" :icon="More" :stroke-width="3.5" size="20" />
</n-dropdown>
</div> </div>
</div> </div>
</template> </template>

View File

@ -74,6 +74,8 @@
"edit_conn_group": "Edit Group", "edit_conn_group": "Edit Group",
"rename_conn_group": "Rename Group", "rename_conn_group": "Rename Group",
"remove_conn_group": "Delete Group", "remove_conn_group": "Delete Group",
"import_conn": "Import Connections...",
"export_conn": "Export Connections...",
"ttl": "TTL", "ttl": "TTL",
"forever": "Forever", "forever": "Forever",
"rename_key": "Rename Key", "rename_key": "Rename Key",

View File

@ -74,6 +74,8 @@
"edit_conn_group": "编辑分组", "edit_conn_group": "编辑分组",
"rename_conn_group": "重命名分组", "rename_conn_group": "重命名分组",
"remove_conn_group": "删除分组", "remove_conn_group": "删除分组",
"import_conn": "导入连接...",
"export_conn": "导出连接...",
"ttl": "TTL", "ttl": "TTL",
"forever": "永久", "forever": "永久",
"rename_key": "重命名键", "rename_key": "重命名键",

View File

@ -4,7 +4,9 @@ import {
CreateGroup, CreateGroup,
DeleteConnection, DeleteConnection,
DeleteGroup, DeleteGroup,
ExportConnections,
GetConnection, GetConnection,
ImportConnections,
ListConnection, ListConnection,
RenameGroup, RenameGroup,
SaveConnection, SaveConnection,
@ -15,6 +17,7 @@ import {
import { ConnectionType } from '@/consts/connection_type.js' import { ConnectionType } from '@/consts/connection_type.js'
import { KeyViewType } from '@/consts/key_view_type.js' import { KeyViewType } from '@/consts/key_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { i18nGlobal } from '@/utils/i18n.js'
const useConnectionStore = defineStore('connections', { const useConnectionStore = defineStore('connections', {
/** /**
@ -399,6 +402,34 @@ const useConnectionStore = defineStore('connections', {
} }
return { success: true } 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'))
},
}, },
}) })