feat: support import/export connection profiles
This commit is contained in:
parent
9402af2433
commit
022ee20eed
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "重命名键",
|
||||||
|
|
|
@ -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'))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue