From ffc50a7fcc381086831379ff593a994c5bb621cd Mon Sep 17 00:00:00 2001 From: tiny-craft <137850705+tiny-craft@users.noreply.github.com> Date: Fri, 29 Sep 2023 23:02:24 +0800 Subject: [PATCH] refactor: move the handling of String viewing methods to backend feat: add view as gzip/deflate #30 --- backend/services/connection_service.go | 25 +- backend/services/preferences_service.go | 6 +- backend/storage/preferences.go | 3 + backend/types/view_type.go | 11 + backend/utils/string/convert.go | 275 ++++++++++++++++++ .../src/components/content/ContentPane.vue | 6 +- .../content_value/ContentValueString.vue | 62 ++-- .../src/components/dialogs/NewKeyDialog.vue | 11 +- frontend/src/consts/value_view_type.js | 7 +- frontend/src/stores/connections.js | 17 +- frontend/src/stores/preferences.js | 2 +- frontend/src/stores/tab.js | 5 +- frontend/src/utils/check_string_format.js | 25 -- frontend/src/utils/string_convert.js | 70 ----- main.go | 10 + 15 files changed, 379 insertions(+), 156 deletions(-) create mode 100644 backend/types/view_type.go create mode 100644 backend/utils/string/convert.go delete mode 100644 frontend/src/utils/check_string_format.js delete mode 100644 frontend/src/utils/string_convert.js diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index f82184c..efff948 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -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) diff --git a/backend/services/preferences_service.go b/backend/services/preferences_service.go index 4c92cbf..7e06569 100644 --- a/backend/services/preferences_service.go +++ b/backend/services/preferences_service.go @@ -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, }) } diff --git a/backend/storage/preferences.go b/backend/storage/preferences.go index 1590d44..1db7143 100644 --- a/backend/storage/preferences.go +++ b/backend/storage/preferences.go @@ -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) } diff --git a/backend/types/view_type.go b/backend/types/view_type.go new file mode 100644 index 0000000..bf0180a --- /dev/null +++ b/backend/types/view_type.go @@ -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" diff --git a/backend/utils/string/convert.go b/backend/utils/string/convert.go new file mode 100644 index 0000000..7e5d286 --- /dev/null +++ b/backend/utils/string/convert.go @@ -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 +} diff --git a/frontend/src/components/content/ContentPane.vue b/frontend/src/components/content/ContentPane.vue index 568545c..9ef78a9 100644 --- a/frontend/src/components/content/ContentPane.vue +++ b/frontend/src/components/content/ContentPane.vue @@ -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) } @@ -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" /> diff --git a/frontend/src/components/content_value/ContentValueString.vue b/frontend/src/components/content_value/ContentValueString.vue index 538b63c..2ecf655 100644 --- a/frontend/src/components/content_value/ContentValueString.vue +++ b/frontend/src/components/content_value/ContentValueString.vue @@ -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} - */ -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 () => {
{{ $t('interface.view_as') }} - +
@@ -188,7 +176,7 @@ const onSaveValue = async () => {
- + 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) diff --git a/frontend/src/consts/value_view_type.js b/frontend/src/consts/value_view_type.js index bcc0582..d101788 100644 --- a/frontend/src/consts/value_view_type.js +++ b/frontend/src/consts/value_view_type.js @@ -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', } diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index 948cfa6..a4a1a05 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -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) diff --git a/frontend/src/stores/preferences.js b/frontend/src/stores/preferences.js index 84a783a..199a2ea 100644 --- a/frontend/src/stores/preferences.js +++ b/frontend/src/stores/preferences.js @@ -38,7 +38,7 @@ const usePreferencesStore = defineStore('preferences', { }, general: { theme: 'auto', - language: 'en', + language: 'auto', font: '', fontSize: 14, useSysProxy: false, diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js index a9e8300..8ed3d9b 100644 --- a/frontend/src/stores/tab.js +++ b/frontend/src/stores/tab.js @@ -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 }, diff --git a/frontend/src/utils/check_string_format.js b/frontend/src/utils/check_string_format.js deleted file mode 100644 index c70f1bb..0000000 --- a/frontend/src/utils/check_string_format.js +++ /dev/null @@ -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 -} diff --git a/frontend/src/utils/string_convert.js b/frontend/src/utils/string_convert.js deleted file mode 100644 index 8d08d2f..0000000 --- a/frontend/src/utils/string_convert.js +++ /dev/null @@ -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(' ') -} diff --git a/main.go b/main.go index b159bdf..5cc4ba4 100644 --- a/main.go +++ b/main.go @@ -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)