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"
"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
}

View File

@ -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
}
}
</script>
<template>
@ -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()" />
<icon-button
:button-class="['nav-pane-func-btn']"
:icon="AddGroup"
size="20"
stroke-width="4"
:stroke-width="3.5"
t-tooltip="interface.new_group"
@click="dialogStore.openNewGroupDialog()" />
<n-divider vertical />
@ -39,6 +63,15 @@ const filterPattern = ref('')
<n-icon :component="Filter" size="20" />
</template>
</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>
</template>

View File

@ -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",

View File

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

View File

@ -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'))
},
},
})