feat: support import/export connection profiles
This commit is contained in:
parent
9402af2433
commit
022ee20eed
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
"edit_conn_group": "编辑分组",
|
||||
"rename_conn_group": "重命名分组",
|
||||
"remove_conn_group": "删除分组",
|
||||
"import_conn": "导入连接...",
|
||||
"export_conn": "导出连接...",
|
||||
"ttl": "TTL",
|
||||
"forever": "永久",
|
||||
"rename_key": "重命名键",
|
||||
|
|
|
@ -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'))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue