perf: support custom ttl when import keys
This commit is contained in:
parent
1d1fab54d8
commit
13dbc9b3b6
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}个"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
Loading…
Reference in New Issue