refactor: move the handling of String viewing methods to backend

feat: add view as gzip/deflate #30
This commit is contained in:
tiny-craft 2023-09-29 23:02:24 +08:00
parent 7aba27e5f9
commit ffc50a7fcc
15 changed files with 379 additions and 156 deletions

View File

@ -17,6 +17,7 @@ import (
"tinyrdm/backend/types"
maputil "tinyrdm/backend/utils/map"
redis2 "tinyrdm/backend/utils/redis"
strutil "tinyrdm/backend/utils/string"
)
type cmdHistoryItem struct {
@ -476,7 +477,7 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
}
// GetKeyValue get value by key
func (c *connectionService) GetKeyValue(connName string, db int, key string) (resp types.JSResp) {
func (c *connectionService) GetKeyValue(connName string, db int, key, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
@ -512,7 +513,9 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
var cursor uint64
switch strings.ToLower(keyType) {
case "string":
value, err = rdb.Get(ctx, key).Result()
var str string
str, err = rdb.Get(ctx, key).Result()
value, viewAs = strutil.ConvertTo(str, viewAs)
size, _ = rdb.StrLen(ctx, key).Result()
case "list":
value, err = rdb.LRange(ctx, key, 0, -1).Result()
@ -601,17 +604,18 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
}
resp.Success = true
resp.Data = map[string]any{
"type": keyType,
"ttl": ttl,
"value": value,
"size": size,
"type": keyType,
"ttl": ttl,
"value": value,
"size": size,
"viewAs": viewAs,
}
return
}
// SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64) (resp types.JSResp) {
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
@ -632,7 +636,12 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
resp.Msg = "invalid string value"
return
} else {
_, err = rdb.Set(ctx, key, str, 0).Result()
var saveStr string
if saveStr, err = strutil.SaveAs(str, viewAs); err != nil {
resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
return
}
_, err = rdb.Set(ctx, key, saveStr, 0).Result()
// set expiration lonely, not "keepttl"
if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration)

View File

@ -104,10 +104,8 @@ func (p *preferencesService) GetAppVersion() (resp types.JSResp) {
func (p *preferencesService) SaveWindowSize(width, height int) {
p.SetPreferences(map[string]any{
"behavior": map[string]any{
"window_width": width,
"window_height": height,
},
"behavior.window_width": width,
"behavior.window_height": height,
})
}

View File

@ -3,6 +3,7 @@ package storage
import (
"fmt"
"gopkg.in/yaml.v3"
"log"
"strings"
"sync"
"tinyrdm/backend/consts"
@ -158,10 +159,12 @@ func (p *PreferencesStorage) SetPreferencesN(values map[string]any) error {
pf := p.getPreferences()
for path, v := range values {
log.Println("path", path, v)
if err := p.setPreferences(pf, path, v); err != nil {
return err
}
}
log.Println("after save", pf)
return p.savePreferences(pf)
}

View File

@ -0,0 +1,11 @@
package types
const PLAIN_TEXT = "Plain Text"
const JSON = "JSON"
const BASE64_TEXT = "Base64 Text"
const BASE64_JSON = "Base64 JSON"
const HEX = "Hex"
const BINARY = "Binary"
const GZIP = "GZip"
const GZIP_JSON = "GZip JSON"
const DEFLATE = "Deflate"

View File

@ -0,0 +1,275 @@
package strutil
import (
"bytes"
"compress/flate"
"compress/gzip"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"strings"
"tinyrdm/backend/types"
)
// ConvertTo convert string to specified type
// @param targetType empty string indicates automatic detection of the string type
func ConvertTo(str, targetType string) (value, resultType string) {
if len(str) <= 0 {
// empty content
if len(targetType) <= 0 {
resultType = types.PLAIN_TEXT
} else {
resultType = targetType
}
return
}
switch targetType {
case types.PLAIN_TEXT:
value = str
resultType = targetType
return
case types.JSON:
value, _ = decodeJson(str)
resultType = targetType
return
case types.BASE64_TEXT, types.BASE64_JSON:
if base64Str, ok := decodeBase64(str); ok {
if targetType == types.BASE64_JSON {
value, _ = decodeJson(base64Str)
} else {
value = base64Str
}
} else {
value = str
}
resultType = targetType
return
case types.HEX:
if hexStr, ok := decodeHex(str); ok {
log.Print(hexStr)
value = hexStr
} else {
value = str
}
resultType = targetType
return
case types.BINARY:
var binary strings.Builder
for _, char := range str {
binary.WriteString(fmt.Sprintf("%08b", int(char)))
}
value = binary.String()
resultType = targetType
return
case types.GZIP, types.GZIP_JSON:
if gzipStr, ok := decodeGZip(str); ok {
if targetType == types.BASE64_JSON {
value, _ = decodeJson(gzipStr)
} else {
value = gzipStr
}
} else {
value = str
}
resultType = targetType
return
case types.DEFLATE:
value, _ = decodeDeflate(str)
resultType = targetType
return
}
// type isn't specified or unknown, try to automatically detect and return converted value
return autoToType(str)
}
// attempt automatic convert to possible types
// if no conversion is possible, it will return the origin string value and "plain text" type
func autoToType(str string) (value, resultType string) {
if len(str) > 0 {
var ok bool
if value, ok = decodeJson(str); ok {
resultType = types.JSON
return
}
if value, ok = decodeBase64(str); ok {
if value, ok = decodeJson(value); ok {
resultType = types.BASE64_JSON
return
}
resultType = types.BASE64_TEXT
return
}
if value, ok = decodeGZip(str); ok {
resultType = types.GZIP
return
}
if value, ok = decodeDeflate(str); ok {
resultType = types.DEFLATE
return
}
}
value = str
resultType = types.PLAIN_TEXT
return
}
func decodeJson(str string) (string, bool) {
var data any
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
if err := json.Unmarshal([]byte(str), &data); err == nil {
var jsonByte []byte
if jsonByte, err = json.MarshalIndent(data, "", " "); err == nil {
return string(jsonByte), true
}
}
}
return str, false
}
func decodeBase64(str string) (string, bool) {
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
return string(decodedStr), true
}
return str, false
}
func decodeHex(str string) (string, bool) {
encodeStr := hex.EncodeToString([]byte(str))
var resultStr strings.Builder
for i := 0; i < len(encodeStr); i += 2 {
resultStr.WriteString("\\x")
resultStr.WriteString(encodeStr[i : i+2])
}
return resultStr.String(), true
}
func decodeGZip(str string) (string, bool) {
if reader, err := gzip.NewReader(strings.NewReader(str)); err == nil {
defer reader.Close()
var decompressed []byte
if decompressed, err = io.ReadAll(reader); err == nil {
return string(decompressed), true
}
}
return str, false
}
func decodeDeflate(str string) (string, bool) {
reader := flate.NewReader(strings.NewReader(str))
defer reader.Close()
if decompressed, err := io.ReadAll(reader); err == nil {
return string(decompressed), true
}
return str, false
}
func SaveAs(str, targetType string) (value string, err error) {
switch targetType {
case types.PLAIN_TEXT:
return str, nil
case types.BASE64_TEXT:
base64Str, _ := encodeBase64(str)
return base64Str, nil
case types.JSON, types.BASE64_JSON, types.GZIP_JSON:
if jsonStr, ok := encodeJson(str); ok {
if targetType == types.BASE64_JSON {
base64Str, _ := encodeBase64(jsonStr)
return base64Str, nil
} else {
return jsonStr, nil
}
} else {
return str, errors.New("invalid json")
}
case types.GZIP:
if gzipStr, ok := encodeGZip(str); ok {
return gzipStr, nil
} else {
return str, errors.New("fail to build gzip data")
}
case types.DEFLATE:
if deflateStr, ok := encodeDeflate(str); ok {
return deflateStr, nil
} else {
return str, errors.New("fail to build deflate data")
}
}
return str, errors.New("fail to save with unknown error")
}
func encodeJson(str string) (string, bool) {
var data any
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
if err := json.Unmarshal([]byte(str), &data); err == nil {
var jsonByte []byte
if jsonByte, err = json.Marshal(data); err == nil {
return string(jsonByte), true
}
}
}
return str, false
}
func encodeBase64(str string) (string, bool) {
return base64.StdEncoding.EncodeToString([]byte(str)), true
}
func encodeGZip(str string) (string, bool) {
var compress = func(b []byte) (string, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
if _, err := writer.Write([]byte(str)); err != nil {
writer.Close()
return "", err
}
writer.Close()
return string(buf.Bytes()), nil
}
if gzipStr, err := compress([]byte(str)); err == nil {
return gzipStr, true
}
return str, false
}
func encodeDeflate(str string) (string, bool) {
var compress = func(b []byte) (string, error) {
var buf bytes.Buffer
writer, err := flate.NewWriter(&buf, flate.DefaultCompression)
if err != nil {
return "", err
}
if _, err = writer.Write([]byte(str)); err != nil {
writer.Close()
return "", err
}
writer.Close()
return string(buf.Bytes()), nil
}
if deflateStr, err := compress([]byte(str)); err == nil {
return deflateStr, true
}
return str, false
}

View File

@ -98,6 +98,7 @@ const tabContent = computed(() => {
ttl: tab.ttl,
value: tab.value,
size: tab.size || 0,
viewAs: tab.viewAs,
}
})
@ -122,7 +123,7 @@ const onReloadKey = async () => {
if (tab == null || isEmpty(tab.key)) {
return null
}
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key)
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs)
}
</script>
@ -153,7 +154,8 @@ const onReloadKey = async () => {
:name="tabContent.name"
:ttl="tabContent.ttl"
:value="tabContent.value"
:size="tabContent.size" />
:size="tabContent.size"
:view-as="tabContent.viewAs" />
</div>
</template>

View File

@ -8,12 +8,10 @@ import { useThemeVars } from 'naive-ui'
import { types } from '@/consts/value_view_type.js'
import Close from '@/components/icons/Close.vue'
import Edit from '@/components/icons/Edit.vue'
import { IsJson } from '@/utils/check_string_format.js'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { map, toLower } from 'lodash'
import useConnectionStore from 'stores/connections.js'
import { fromBase64, fromBase64Json, toBinary, toHex, toJsonText } from '@/utils/string_convert.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -28,6 +26,10 @@ const props = defineProps({
},
value: String,
size: Number,
viewAs: {
type: String,
default: types.PLAIN_TEXT,
},
})
const viewOption = computed(() =>
@ -38,15 +40,15 @@ const viewOption = computed(() =>
}
}),
)
const viewAs = ref(types.PLAIN_TEXT)
// const viewAs = ref(types.PLAIN_TEXT)
const autoDetectFormat = () => {
// auto check format when loaded
if (IsJson(props.value)) {
viewAs.value = types.JSON
} else {
viewAs.value = types.PLAIN_TEXT
}
// if (IsJson(props.value)) {
// viewAs.value = types.JSON
// } else {
// viewAs.value = types.PLAIN_TEXT
// }
}
onMounted(() => {
@ -60,44 +62,25 @@ watch(
)
const keyType = redisTypes.STRING
/**
* view value
* @type {ComputedRef<string>}
*/
const viewValue = computed(() => {
switch (viewAs.value) {
case types.PLAIN_TEXT:
return props.value
case types.JSON:
return toJsonText(props.value)
case types.BASE64_TO_TEXT:
return fromBase64(props.value)
case types.BASE64_TO_JSON:
return fromBase64Json(props.value)
case types.HEX:
return toHex(props.value)
case types.BINARY:
return toBinary(props.value)
default:
return props.value
}
})
const viewLanguage = computed(() => {
switch (viewAs.value) {
switch (props.viewAs) {
case types.JSON:
case types.BASE64_TO_JSON:
case types.BASE64_JSON:
return 'json'
default:
return 'plaintext'
}
})
const onViewTypeUpdate = (viewType) => {
connectionStore.loadKeyValue(props.name, props.db, props.keyPath, viewType)
}
/**
* Copy value
*/
const onCopyValue = () => {
ClipboardSetText(viewValue.value)
ClipboardSetText(props.value)
.then((succ) => {
if (succ) {
$message.success(i18n.t('dialogue.copy_succ'))
@ -111,7 +94,7 @@ const onCopyValue = () => {
const editValue = ref('')
const inEdit = ref(false)
const onEditValue = () => {
editValue.value = viewValue.value
editValue.value = props.value
inEdit.value = true
}
@ -134,6 +117,7 @@ const onSaveValue = async () => {
toLower(keyType),
editValue.value,
-1,
props.viewAs,
)
if (success) {
await connectionStore.loadKeyValue(props.name, props.db, props.keyPath)
@ -155,7 +139,11 @@ const onSaveValue = async () => {
<content-toolbar :db="props.db" :key-path="keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
<div class="tb2 flex-box-h">
<n-text>{{ $t('interface.view_as') }}</n-text>
<n-select v-model:value="viewAs" :options="viewOption" style="width: 200px" />
<n-select
:value="props.viewAs"
:options="viewOption"
style="width: 200px"
@update:value="onViewTypeUpdate" />
<div class="flex-item-expand"></div>
<n-button-group v-if="!inEdit">
<n-button :focusable="false" @click="onCopyValue">
@ -188,7 +176,7 @@ const onSaveValue = async () => {
</div>
<div class="value-wrapper flex-item-expand flex-box-v">
<n-scrollbar v-if="!inEdit" class="flex-item-expand">
<n-code :code="viewValue" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
<n-code :code="props.value" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
</n-scrollbar>
<n-input
v-else

View File

@ -1,6 +1,7 @@
<script setup>
import { computed, h, reactive, ref, watch } from 'vue'
import { types, typesColor } from '@/consts/support_redis_type.js'
import { types as viewTypes } from '@/consts/value_view_type.js'
import useDialog from 'stores/dialog'
import { isEmpty, keys, map } from 'lodash'
import NewStringValue from '@/components/new_value/NewStringValue.vue'
@ -116,7 +117,15 @@ const onAdd = async () => {
if (value == null) {
value = defaultValue[type]
}
const { success, msg, nodeKey } = await connectionStore.setKey(server, db, key, type, value, ttl)
const { success, msg, nodeKey } = await connectionStore.setKey(
server,
db,
key,
type,
value,
ttl,
viewTypes.PLAIN_TEXT,
)
if (success) {
// select current key
tabStore.setSelectedKeys(server, nodeKey)

View File

@ -5,8 +5,11 @@
export const types = {
PLAIN_TEXT: 'Plain Text',
JSON: 'JSON',
BASE64_TO_TEXT: 'Base64 To Text',
BASE64_TO_JSON: 'Base64 To JSON',
BASE64_TEXT: 'Base64 Text',
BASE64_JSON: 'Base64 JSON',
HEX: 'Hex',
BINARY: 'Binary',
GZIP: 'GZip',
GZIP_JSON: 'GZip JSON',
DEFLATE: 'Deflate',
}

View File

@ -525,14 +525,15 @@ const useConnectionStore = defineStore('connections', {
* @param {string} server
* @param {number} db
* @param {string} [key] when key is null or blank, update tab to display normal content (blank content or server status)
* @param {string} [viewType]
*/
async loadKeyValue(server, db, key) {
async loadKeyValue(server, db, key, viewType) {
try {
const tab = useTabStore()
if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key)
const { data, success, msg } = await GetKeyValue(server, db, key, viewType)
if (success) {
const { type, ttl, value, size } = data
const { type, ttl, value, size, viewAs } = data
tab.upsertTab({
server,
db,
@ -541,11 +542,16 @@ const useConnectionStore = defineStore('connections', {
key,
value,
size,
viewAs,
})
return
} else {
if (!isEmpty(msg)) {
$message.error('load key fail: ' + msg)
}
// its danger to delete "non-exists" key, just remove from tree view
await this.deleteKey(server, db, key, true)
// TODO: show key not found page?
}
}
@ -852,11 +858,12 @@ const useConnectionStore = defineStore('connections', {
* @param {string} keyType
* @param {any} value
* @param {number} ttl
* @param {string} [viewAs]
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
*/
async setKey(connName, db, key, keyType, value, ttl) {
async setKey(connName, db, key, keyType, value, ttl, viewAs) {
try {
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs)
if (success) {
// update tree view data
const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)

View File

@ -38,7 +38,7 @@ const usePreferencesStore = defineStore('preferences', {
},
general: {
theme: 'auto',
language: 'en',
language: 'auto',
font: '',
fontSize: 14,
useSysProxy: false,

View File

@ -85,8 +85,9 @@ const useTabStore = defineStore('tab', {
* @param {string} [key]
* @param {number} [size]
* @param {*} [value]
* @param {string} [viewAs]
*/
upsertTab({ server, db, type, ttl, key, size, value }) {
upsertTab({ server, db, type, ttl, key, size, value, viewAs }) {
let tabIndex = findIndex(this.tabList, { name: server })
if (tabIndex === -1) {
this.tabList.push({
@ -98,6 +99,7 @@ const useTabStore = defineStore('tab', {
key,
size,
value,
viewAs,
})
tabIndex = this.tabList.length - 1
}
@ -112,6 +114,7 @@ const useTabStore = defineStore('tab', {
tab.key = key
tab.size = size
tab.value = value
tab.viewAs = viewAs
this._setActivatedIndex(tabIndex, true)
// this.activatedTab = tab.name
},

View File

@ -1,25 +0,0 @@
import { endsWith, isEmpty, size, startsWith } from 'lodash'
export const IsRedisKey = (str, separator) => {
if (isEmpty(separator)) {
separator = ':'
}
}
/**
* check string is json
* @param str
* @returns {boolean}
* @constructor
*/
export const IsJson = (str) => {
if (size(str) >= 2) {
if (startsWith(str, '{') && endsWith(str, '}')) {
return true
}
if (startsWith(str, '[') && endsWith(str, ']')) {
return true
}
}
return false
}

View File

@ -1,70 +0,0 @@
import { map, padStart } from 'lodash'
/**
* convert string to json
* @param str
* @return {string}
*/
export const toJsonText = (str) => {
try {
const jsonObj = JSON.parse(str)
return JSON.stringify(jsonObj, null, 2)
} catch (e) {
return str
}
}
/**
* convert string from base64
* @param str
* @return {string}
*/
export const fromBase64 = (str) => {
try {
return atob(str)
} catch (e) {
return str
}
}
/**
* convert string from base64 to json
* @param str
* @return {string}
*/
export const fromBase64Json = (str) => {
try {
const text = atob(str)
const jsonObj = JSON.parse(text)
return JSON.stringify(jsonObj, null, 2)
} catch (e) {
return str
}
}
/**
* convert string to hex string
* @param str
* @return {string}
*/
export const toHex = (str) => {
const hexArr = map(str, (char) => {
const charCode = char.charCodeAt(0)
return charCode.toString(16)
})
return hexArr.join(' ')
}
/**
* convert string to binary string
* @param str
* @return {string}
*/
export const toBinary = (str) => {
const codeUnits = map(str, (char) => {
let code = char.charCodeAt(0).toString(2)
code = padStart(code, 8, '0')
return code
})
return codeUnits.join(' ')
}

10
main.go
View File

@ -57,6 +57,16 @@ func main() {
app.startup(ctx)
connSvc.Start(ctx)
},
//OnBeforeClose: func(ctx context.Context) (prevent bool) {
// // save current window size
// width, height := runtime2.WindowGetSize(ctx)
// if width > 0 && height > 0 {
// if w, h := prefSvc.GetWindowSize(); w != width || h != height {
// prefSvc.SaveWindowSize(width, height)
// }
// }
// return false
//},
OnShutdown: func(ctx context.Context) {
// save current window size
width, height := runtime2.WindowGetSize(ctx)