feat: add expire time data for import/export handle

This commit is contained in:
Lykin 2023-12-27 17:41:51 +08:00
parent 3fe8767c44
commit f597002378
11 changed files with 84 additions and 29 deletions

View File

@ -2056,7 +2056,7 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
} }
// ExportKey export keys // ExportKey export keys
func (b *browserService) ExportKey(server string, db int, ks []any, path string) (resp types.JSResp) { func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (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 {
@ -2111,7 +2111,15 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
canceled = true canceled = true
break break
} }
if err = writer.Write([]string{hex.EncodeToString([]byte(key)), hex.EncodeToString(content)}); err != nil { record := []string{hex.EncodeToString([]byte(key)), hex.EncodeToString(content)}
if includeExpire {
if dur, ttlErr := client.PTTL(ctx, key).Result(); ttlErr == nil && dur > 0 {
record = append(record, strconv.FormatInt(time.Now().Add(dur).UnixMilli(), 10))
} else {
record = append(record, "-1")
}
}
if err = writer.Write(record); err != nil {
failed += 1 failed += 1
} else { } else {
exported += 1 exported += 1
@ -2133,7 +2141,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) (resp types.JSResp) { func (b *browserService) ImportCSV(server string, db int, path string, conflict int, includeExpire bool) (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 {
@ -2169,7 +2177,7 @@ 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 int64 var ttl 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)
@ -2192,19 +2200,19 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
continue continue
} }
// get ttl // get ttl
if len(line) > 2 { if includeExpire && len(line) > 2 {
if ttl, readErr = strconv.ParseInt(line[2], 10, 64); readErr != nil { if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 {
ttl = redis.KeepTTL ttl = time.UnixMilli(expire).Sub(time.Now())
} }
} }
if conflict == 0 { if conflict == 0 {
readErr = client.RestoreReplace(ctx, string(key), time.Duration(ttl), string(value)).Err() readErr = client.RestoreReplace(ctx, string(key), ttl, 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, time.Duration(ttl), string(value)).Err() readErr = client.Restore(ctx, keyStr, ttl, string(value)).Err()
} else { } else {
readErr = errors.New("key existed") readErr = errors.New("key existed")
} }

View File

@ -11,6 +11,14 @@ const props = defineProps({
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const onInput = (val) => {
emit('update:value', val)
}
const onClear = () => {
emit('update:value', '')
}
const handleSelectFile = async () => { const handleSelectFile = async () => {
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext]) const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
if (success) { if (success) {
@ -24,7 +32,13 @@ const handleSelectFile = async () => {
<template> <template>
<n-input-group> <n-input-group>
<n-input v-model:value="props.value" :disabled="props.disabled" :placeholder="placeholder" clearable /> <n-input
:disabled="props.disabled"
:placeholder="placeholder"
:value="props.value"
clearable
@clear="onClear"
@input="onInput" />
<n-button :disabled="props.disabled" :focusable="false" @click="handleSelectFile">...</n-button> <n-button :disabled="props.disabled" :focusable="false" @click="handleSelectFile">...</n-button>
</n-input-group> </n-input-group>
</template> </template>

View File

@ -11,6 +11,10 @@ const props = defineProps({
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const onInput = (val) => {
emit('update:value', val)
}
const onClear = () => { const onClear = () => {
emit('update:value', '') emit('update:value', '')
} }
@ -29,10 +33,11 @@ const handleSaveFile = async () => {
<template> <template>
<n-input-group> <n-input-group>
<n-input <n-input
v-model:value="props.value" :value="props.value"
:disabled="props.disabled" :disabled="props.disabled"
:placeholder="placeholder" :placeholder="placeholder"
clearable clearable
@input="onInput"
@clear="onClear" /> @clear="onClear" />
<n-button :disabled="props.disabled" :focusable="false" @click="handleSaveFile">...</n-button> <n-button :disabled="props.disabled" :focusable="false" @click="handleSaveFile">...</n-button>
</n-input-group> </n-input-group>

View File

@ -11,6 +11,7 @@ import dayjs from 'dayjs'
const exportKeyForm = reactive({ const exportKeyForm = reactive({
server: '', server: '',
db: 0, db: 0,
expire: false,
keys: [], keys: [],
file: '', file: '',
}) })
@ -24,7 +25,9 @@ watchEffect(() => {
const { server, db, keys } = dialogStore.exportKeyParam const { server, db, keys } = dialogStore.exportKeyParam
exportKeyForm.server = server exportKeyForm.server = server
exportKeyForm.db = db exportKeyForm.db = db
exportKeyForm.ttl = false
exportKeyForm.keys = keys exportKeyForm.keys = keys
exportKeyForm.file = ''
exporting.value = false exporting.value = false
} }
}) })
@ -41,8 +44,8 @@ const i18n = useI18n()
const onConfirmExport = async () => { const onConfirmExport = async () => {
try { try {
exporting.value = true exporting.value = true
const { server, db, keys, file } = exportKeyForm const { server, db, keys, file, expire } = exportKeyForm
browserStore.exportKeys(server, db, keys, file).catch((e) => {}) browserStore.exportKeys(server, db, keys, file, expire).catch((e) => {})
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return return
@ -77,6 +80,11 @@ const onClose = () => {
<n-input :autofocus="false" :value="exportKeyForm.db.toString()" readonly /> <n-input :autofocus="false" :value="exportKeyForm.db.toString()" readonly />
</n-form-item-gi> </n-form-item-gi>
</n-grid> </n-grid>
<n-form-item :label="$t('dialogue.export.export_expire_title')">
<n-checkbox v-model:checked="exportKeyForm.expire" :autofocus="false">
{{ $t('dialogue.export.export_expire') }}
</n-checkbox>
</n-form-item>
<n-form-item :label="$t('dialogue.export.save_file')" required> <n-form-item :label="$t('dialogue.export.save_file')" required>
<file-save-input <file-save-input
v-model:value="exportKeyForm.file" v-model:value="exportKeyForm.file"
@ -94,12 +102,14 @@ const onClose = () => {
<template #action> <template #action>
<div class="flex-item n-dialog__action"> <div class="flex-item n-dialog__action">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button> <n-button :disabled="loading" :focusable="false" @click="onClose">
{{ $t('common.cancel') }}
</n-button>
<n-button <n-button
:disabled="!exportEnable" :disabled="!exportEnable"
:focusable="false" :focusable="false"
:loading="loading" :loading="loading"
type="error" type="primary"
@click="onConfirmExport"> @click="onConfirmExport">
{{ $t('dialogue.export.export') }} {{ $t('dialogue.export.export') }}
</n-button> </n-button>

View File

@ -9,6 +9,7 @@ import FileOpenInput from '@/components/common/FileOpenInput.vue'
const importKeyForm = reactive({ const importKeyForm = reactive({
server: '', server: '',
db: 0, db: 0,
expire: true,
file: '', file: '',
type: 0, type: 0,
conflict: 0, conflict: 0,
@ -23,6 +24,7 @@ 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.file = '' importKeyForm.file = ''
importKeyForm.type = 0 importKeyForm.type = 0
importKeyForm.conflict = 0 importKeyForm.conflict = 0
@ -49,8 +51,8 @@ const importEnable = computed(() => {
const onConfirmImport = async () => { const onConfirmImport = async () => {
try { try {
importing.value = true importing.value = true
const { server, db, file, conflict } = importKeyForm const { server, db, file, conflict, expire } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict).catch((e) => {}) browserStore.importKeysFromCSVFile(server, db, file, conflict, expire).catch((e) => {})
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return return
@ -91,6 +93,11 @@ const onClose = () => {
:placeholder="$t('dialogue.import.open_csv_file_tip')" :placeholder="$t('dialogue.import.open_csv_file_tip')"
ext="csv" /> ext="csv" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')">
<n-checkbox v-model:checked="importKeyForm.expire" :autofocus="false">
{{ $t('dialogue.import.import_expire') }}
</n-checkbox>
</n-form-item>
<n-form-item :label="$t('dialogue.import.conflict_handle')"> <n-form-item :label="$t('dialogue.import.conflict_handle')">
<n-radio-group v-model:value="importKeyForm.conflict"> <n-radio-group v-model:value="importKeyForm.conflict">
<n-radio-button <n-radio-button
@ -110,7 +117,7 @@ const onClose = () => {
:disabled="!importEnable" :disabled="!importEnable"
:focusable="false" :focusable="false"
:loading="loading" :loading="loading"
type="error" type="primary"
@click="onConfirmImport"> @click="onConfirmImport">
{{ $t('dialogue.export.export') }} {{ $t('dialogue.export.export') }}
</n-button> </n-button>

View File

@ -11,19 +11,19 @@ const props = defineProps({
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"> <svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24" d="M6 24V42H42V24"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M33 23L24 32L15 23" d="M33 15L24 6L15 15"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M23.9917 6V32" d="M24 6V32"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />

View File

@ -11,19 +11,19 @@ const props = defineProps({
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"> <svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M6 24V42H42V24" d="M6 24.0083V42H42V24"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M33 15L24 6L15 15" d="M33 23L24 32L15 23"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M24 6V32" d="M23.9917 6V32"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />

View File

@ -66,6 +66,7 @@ const dbSelectOptions = computed(() => {
const moreOptions = computed(() => { const moreOptions = computed(() => {
return [ return [
{ key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) }, { key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) },
{ key: 'divider', type: 'divider' },
{ key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) }, { key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
{ {
key: 'disconnect', key: 'disconnect',

View File

@ -282,6 +282,8 @@
}, },
"import": { "import": {
"name": "Import Data", "name": "Import Data",
"export_expire_title": "Expiration",
"export_expire": "Export Expiration Time",
"import": "Import", "import": "Import",
"open_csv_file": "Import File", "open_csv_file": "Import File",
"open_csv_file_tip": "Select the file for import", "open_csv_file_tip": "Select the file for import",
@ -296,6 +298,8 @@
}, },
"upgrade": { "upgrade": {
"title": "New Version Available", "title": "New Version Available",
"import_expire_title": "Expiration",
"import_expire": "Try Import Expiration Time",
"new_version_tip": "A new version({ver}) is available. Download now?", "new_version_tip": "A new version({ver}) is available. Download now?",
"no_update": "You're update to date", "no_update": "You're update to date",
"download_now": "Download", "download_now": "Download",

View File

@ -274,6 +274,8 @@
}, },
"export": { "export": {
"name": "导出数据", "name": "导出数据",
"export_expire_title": "过期时间",
"export_expire": "同时导出过期时间",
"export": "确认导出", "export": "确认导出",
"save_file": "导出路径", "save_file": "导出路径",
"save_file_tip": "选择导出文件保存路径", "save_file_tip": "选择导出文件保存路径",
@ -282,6 +284,8 @@
}, },
"import": { "import": {
"name": "导入数据", "name": "导入数据",
"import_expire_title": "过期时间",
"import_expire": "尝试同时导入过期时间",
"import": "确认导入", "import": "确认导入",
"open_csv_file": "导入文件路径", "open_csv_file": "导入文件路径",
"open_csv_file_tip": "选择需要导入的文件", "open_csv_file_tip": "选择需要导入的文件",

View File

@ -1606,9 +1606,10 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db * @param {number} db
* @param {string[]|number[][]} keys * @param {string[]|number[][]} keys
* @param {string} path * @param {string} path
* @param {boolean} [expire]
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async exportKeys(server, db, keys, path) { async exportKeys(server, db, keys, path, expire) {
const msgRef = $message.loading('', { duration: 0, closable: true }) const msgRef = $message.loading('', { duration: 0, closable: true })
let exported = 0 let exported = 0
let failCount = 0 let failCount = 0
@ -1626,7 +1627,7 @@ const useBrowserStore = defineStore('browser', {
msgRef.onClose = () => { msgRef.onClose = () => {
EventsEmit('export:stop:' + path) EventsEmit('export:stop:' + path)
} }
const { data, success, msg } = await ExportKey(server, db, keys, path) const { data, success, msg } = await ExportKey(server, db, keys, path, expire)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
exported = get(data, 'exported', 0) exported = get(data, 'exported', 0)
@ -1659,10 +1660,11 @@ const useBrowserStore = defineStore('browser', {
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string} path * @param {string} path
* @param {int} conflict * @param {number} conflict
* @param {boolean} [expire]
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async importKeysFromCSVFile(server, db, path, conflict) { async importKeysFromCSVFile(server, db, path, conflict, expire) {
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
@ -1680,7 +1682,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) const { data, success, msg } = await ImportCSV(server, db, path, conflict, expire)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
imported = get(data, 'imported', 0) imported = get(data, 'imported', 0)