perf: support custom ttl when import keys

This commit is contained in:
Lykin 2024-01-05 17:46:12 +08:00
parent 1d1fab54d8
commit 13dbc9b3b6
9 changed files with 138 additions and 51 deletions

View File

@ -2234,7 +2234,7 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
} }
// ImportCSV import data from csv file // ImportCSV import data from csv file
func (b *browserService) ImportCSV(server string, db int, path string, conflict int, includeExpire bool) (resp types.JSResp) { func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (resp types.JSResp) {
// connect a new connection to export keys // connect a new connection to export keys
conf := Connection().getConnection(server) conf := Connection().getConnection(server)
if conf == nil { if conf == nil {
@ -2270,14 +2270,14 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
var line []string var line []string
var readErr error var readErr error
var key, value []byte var key, value []byte
var ttl time.Duration var ttlValue time.Duration
var imported, ignored int64 var imported, ignored int64
var canceled bool var canceled bool
startTime := time.Now().Add(-10 * time.Second) startTime := time.Now().Add(-10 * time.Second)
for { for {
readErr = nil readErr = nil
ttl = redis.KeepTTL ttlValue = redis.KeepTTL
line, readErr = reader.Read() line, readErr = reader.Read()
if readErr != nil { if readErr != nil {
break break
@ -2293,21 +2293,25 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
continue continue
} }
// get ttl // get ttl
if includeExpire && len(line) > 2 { if ttl < 0 {
// use previous
if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 { if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 {
ttl = time.UnixMilli(expire).Sub(time.Now()) ttlValue = time.UnixMilli(expire).Sub(time.Now())
} }
} else if ttl > 0 {
// custom ttl
ttlValue = time.Duration(ttl) * time.Second
} }
if conflict == 0 { if conflict == 0 {
readErr = client.RestoreReplace(ctx, string(key), ttl, string(value)).Err() readErr = client.RestoreReplace(ctx, string(key), ttlValue, string(value)).Err()
} else { } else {
keyStr := string(key) keyStr := string(key)
// go-redis may crash when batch calling restore // go-redis may crash when batch calling restore
// use "exists" to filter first // use "exists" to filter first
if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 { if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 {
readErr = client.Restore(ctx, keyStr, ttl, string(value)).Err() readErr = client.Restore(ctx, keyStr, ttlValue, string(value)).Err()
} else { } else {
readErr = errors.New("key existed") readErr = errors.New("key already existed")
} }
} }
if readErr != nil { if readErr != nil {

View File

@ -0,0 +1,66 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const props = defineProps({
value: {
type: Number,
default: -1,
},
unit: {
type: Number,
default: 1,
},
})
const emit = defineEmits(['update:value', 'update:unit'])
const i18n = useI18n()
const unit = computed(() => [
{ value: 1, label: i18n.t('common.second') },
{
value: 60,
label: i18n.t('common.minute'),
},
{
value: 3600,
label: i18n.t('common.hour'),
},
{
value: 86400,
label: i18n.t('common.day'),
},
])
const unitValue = computed(() => {
switch (props.unit) {
case 60:
return 60
case 3600:
return 3600
case 86400:
return 86400
default:
return 1
}
})
</script>
<template>
<n-input-group>
<n-input-number
:max="Number.MAX_SAFE_INTEGER"
:min="-1"
:show-button="false"
:value="props.value"
class="flex-item-expand"
@update:value="(val) => emit('update:value', val)" />
<n-select
:options="unit"
:value="unitValue"
style="max-width: 150px"
@update:value="(val) => emit('update:unit', val)" />
</n-input-group>
</template>
<style lang="scss" scoped></style>

View File

@ -5,15 +5,18 @@ import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import FileOpenInput from '@/components/common/FileOpenInput.vue' import FileOpenInput from '@/components/common/FileOpenInput.vue'
import TtlInput from '@/components/common/TtlInput.vue'
const importKeyForm = reactive({ const importKeyForm = reactive({
server: '', server: '',
db: 0, db: 0,
expire: true,
reload: true, reload: true,
file: '', file: '',
type: 0, type: 0,
conflict: 0, conflict: 0,
ttlType: 0,
ttl: -1,
ttlUnit: 1,
}) })
const dialogStore = useDialog() const dialogStore = useDialog()
@ -25,11 +28,12 @@ watchEffect(() => {
const { server, db } = dialogStore.importKeyParam const { server, db } = dialogStore.importKeyParam
importKeyForm.server = server importKeyForm.server = server
importKeyForm.db = db importKeyForm.db = db
importKeyForm.expire = true
importKeyForm.reload = true importKeyForm.reload = true
importKeyForm.file = '' importKeyForm.file = ''
importKeyForm.type = 0 importKeyForm.type = 0
importKeyForm.conflict = 0 importKeyForm.conflict = 0
importKeyForm.ttlType = 0
importKeyForm.ttl = -1
importing.value = false importing.value = false
} }
}) })
@ -46,6 +50,21 @@ const conflictOption = computed(() => [
}, },
]) ])
const ttlOption = computed(() => [
{
value: 0,
label: i18n.t('dialogue.import.ttl_include'),
},
{
value: 1,
label: i18n.t('dialogue.import.ttl_ignore'),
},
{
value: 2,
label: i18n.t('dialogue.import.ttl_custom'),
},
])
const importEnable = computed(() => { const importEnable = computed(() => {
return !isEmpty(importKeyForm.file) return !isEmpty(importKeyForm.file)
}) })
@ -53,8 +72,19 @@ const importEnable = computed(() => {
const onConfirmImport = async () => { const onConfirmImport = async () => {
try { try {
importing.value = true importing.value = true
const { server, db, file, conflict, expire, reload } = importKeyForm const { server, db, file, conflict, ttlType, ttl, ttlUnit, reload } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict, expire, reload).catch((e) => {}) let ttlVal = 0
switch (ttlType) {
case 0:
ttlVal = -1
break
case 1:
ttlVal = 0
break
default:
ttlVal = ttl * (ttlUnit || 1)
}
browserStore.importKeysFromCSVFile(server, db, file, conflict, ttlVal, reload).catch((e) => {})
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return return
@ -104,16 +134,22 @@ const onClose = () => {
:value="op.value" /> :value="op.value" />
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')" :show-label="false"> <n-form-item :label="$t('dialogue.import.import_expire_title')">
<n-space :wrap-item="false"> <n-space :wrap-item="false">
<n-checkbox v-model:checked="importKeyForm.expire" :autofocus="false"> <n-radio-group v-model:value="importKeyForm.ttlType">
{{ $t('dialogue.import.import_expire') }} <n-radio-button v-for="(op, i) in ttlOption" :key="i" :label="op.label" :value="op.value" />
</n-checkbox> </n-radio-group>
<n-checkbox v-model:checked="importKeyForm.reload" :autofocus="false"> <ttl-input
{{ $t('dialogue.import.reload') }} v-if="importKeyForm.ttlType === 2"
</n-checkbox> v-model:unit="importKeyForm.ttlUnit"
v-model:value="importKeyForm.ttl" />
</n-space> </n-space>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')" :show-label="false">
<n-checkbox v-model:checked="importKeyForm.reload" :autofocus="false">
{{ $t('dialogue.import.reload') }}
</n-checkbox>
</n-form-item>
</n-form> </n-form>
</n-spin> </n-spin>

View File

@ -4,6 +4,7 @@ import useDialog from 'stores/dialog'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isEmpty, size } from 'lodash' import { isEmpty, size } from 'lodash'
import TtlInput from '@/components/common/TtlInput.vue'
const ttlForm = reactive({ const ttlForm = reactive({
server: '', server: '',
@ -42,22 +43,6 @@ const isBatchAction = computed(() => {
}) })
const i18n = useI18n() const i18n = useI18n()
const unit = computed(() => [
{ value: 1, label: i18n.t('common.second') },
{
value: 60,
label: i18n.t('common.minute'),
},
{
value: 3600,
label: i18n.t('common.hour'),
},
{
value: 86400,
label: i18n.t('common.day'),
},
])
const quickOption = computed(() => [ const quickOption = computed(() => [
{ value: -1, unit: 1, label: i18n.t('interface.forever') }, { value: -1, unit: 1, label: i18n.t('interface.forever') },
{ value: 10, unit: 1, label: `10 ${i18n.t('common.second')}` }, { value: 10, unit: 1, label: `10 ${i18n.t('common.second')}` },
@ -119,15 +104,7 @@ const onConfirm = async () => {
<n-input :value="ttlForm.key" readonly /> <n-input :value="ttlForm.key" readonly />
</n-form-item> </n-form-item>
<n-form-item :label="$t('interface.ttl')" required> <n-form-item :label="$t('interface.ttl')" required>
<n-input-group> <ttl-input v-model:unit="ttlForm.unit" v-model:value="ttlForm.ttl" />
<n-input-number
v-model:value="ttlForm.ttl"
:max="Number.MAX_SAFE_INTEGER"
:min="-1"
:show-button="false"
class="flex-item-expand" />
<n-select v-model:value="ttlForm.unit" :options="unit" style="max-width: 150px" />
</n-input-group>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.ttl.quick_set')" :show-feedback="false"> <n-form-item :label="$t('dialogue.ttl.quick_set')" :show-feedback="false">
<n-space :wrap="true" :wrap-item="false"> <n-space :wrap="true" :wrap-item="false">

View File

@ -565,7 +565,7 @@ watchEffect(
) )
// the NTree node may get incorrect height after change data // the NTree node may get incorrect height after change data
// add key property to force refresh the component and then everything back to normal // add key property for force refresh the component so that everything back to normal
const treeKey = ref(0) const treeKey = ref(0)
defineExpose({ defineExpose({
handleSelectContextMenu, handleSelectContextMenu,

View File

@ -291,7 +291,6 @@
"import": { "import": {
"name": "Import Data", "name": "Import Data",
"import_expire_title": "Expiration", "import_expire_title": "Expiration",
"import_expire": "Import Expiration Time",
"import": "Import", "import": "Import",
"reload": "Reload Keys After Imported", "reload": "Reload Keys After Imported",
"open_csv_file": "Import File", "open_csv_file": "Import File",
@ -299,6 +298,9 @@
"conflict_handle": "Key Conflict Resolution", "conflict_handle": "Key Conflict Resolution",
"conflict_overwrite": "Overwrite", "conflict_overwrite": "Overwrite",
"conflict_ignore": "Ignore", "conflict_ignore": "Ignore",
"ttl_include": "Import From File",
"ttl_ignore": "Do Not Set",
"ttl_custom": "Custom",
"importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}", "importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}",
"import_completed": "Import completed, {success} successes, {ignored} failed" "import_completed": "Import completed, {success} successes, {ignored} failed"
}, },

View File

@ -291,7 +291,6 @@
"import": { "import": {
"name": "导入数据", "name": "导入数据",
"import_expire_title": "过期时间", "import_expire_title": "过期时间",
"import_expire": "包含键过期时间",
"reload": "导入完成后重新载入", "reload": "导入完成后重新载入",
"import": "确认导入", "import": "确认导入",
"open_csv_file": "导入文件路径", "open_csv_file": "导入文件路径",
@ -299,6 +298,9 @@
"conflict_handle": "键冲突处理", "conflict_handle": "键冲突处理",
"conflict_overwrite": "覆盖", "conflict_overwrite": "覆盖",
"conflict_ignore": "忽略", "conflict_ignore": "忽略",
"ttl_include": "尝试导入",
"ttl_ignore": "不设置",
"ttl_custom": "自定义",
"importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}", "importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}",
"import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个" "import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个"
}, },

View File

@ -1738,11 +1738,11 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db * @param {number} db
* @param {string} path * @param {string} path
* @param {number} conflict * @param {number} conflict
* @param {boolean} [expire] * @param {number} [ttl] <0:use previous; ==0: persist; >0: custom ttl
* @param {boolean} [reload] * @param {boolean} [reload]
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async importKeysFromCSVFile(server, db, path, conflict, expire, reload) { async importKeysFromCSVFile(server, db, path, conflict, ttl, reload) {
const msgRef = $message.loading('', { duration: 0, closable: true }) const msgRef = $message.loading('', { duration: 0, closable: true })
let imported = 0 let imported = 0
let ignored = 0 let ignored = 0
@ -1760,7 +1760,7 @@ const useBrowserStore = defineStore('browser', {
msgRef.onClose = () => { msgRef.onClose = () => {
EventsEmit('import:stop:' + path) EventsEmit('import:stop:' + path)
} }
const { data, success, msg } = await ImportCSV(server, db, path, conflict, expire) const { data, success, msg } = await ImportCSV(server, db, path, conflict, ttl)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
imported = get(data, 'imported', 0) imported = get(data, 'imported', 0)

View File

@ -674,7 +674,7 @@ const useTabStore = defineStore('tab', {
* @param {string} server * @param {string} server
* @param {CheckedKey[]} [keys] * @param {CheckedKey[]} [keys]
*/ */
setCheckedKeys(server, keys) { setCheckedKeys(server, keys = null) {
let tab = find(this.tabList, { name: server }) let tab = find(this.tabList, { name: server })
if (tab != null) { if (tab != null) {
if (isEmpty(keys)) { if (isEmpty(keys)) {