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
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
conf := Connection().getConnection(server)
if conf == nil {
@ -2111,7 +2111,15 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
canceled = true
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
} else {
exported += 1
@ -2133,7 +2141,7 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
}
// 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
conf := Connection().getConnection(server)
if conf == nil {
@ -2169,7 +2177,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
var line []string
var readErr error
var key, value []byte
var ttl int64
var ttl time.Duration
var imported, ignored int64
var canceled bool
startTime := time.Now().Add(-10 * time.Second)
@ -2192,19 +2200,19 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
continue
}
// get ttl
if len(line) > 2 {
if ttl, readErr = strconv.ParseInt(line[2], 10, 64); readErr != nil {
ttl = redis.KeepTTL
if includeExpire && len(line) > 2 {
if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 {
ttl = time.UnixMilli(expire).Sub(time.Now())
}
}
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 {
keyStr := string(key)
// go-redis may crash when batch calling restore
// use "exists" to filter first
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 {
readErr = errors.New("key existed")
}

View File

@ -11,6 +11,14 @@ const props = defineProps({
const emit = defineEmits(['update:value'])
const onInput = (val) => {
emit('update:value', val)
}
const onClear = () => {
emit('update:value', '')
}
const handleSelectFile = async () => {
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
if (success) {
@ -24,7 +32,13 @@ const handleSelectFile = async () => {
<template>
<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-input-group>
</template>

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import FileOpenInput from '@/components/common/FileOpenInput.vue'
const importKeyForm = reactive({
server: '',
db: 0,
expire: true,
file: '',
type: 0,
conflict: 0,
@ -23,6 +24,7 @@ watchEffect(() => {
const { server, db } = dialogStore.importKeyParam
importKeyForm.server = server
importKeyForm.db = db
importKeyForm.expire = true
importKeyForm.file = ''
importKeyForm.type = 0
importKeyForm.conflict = 0
@ -49,8 +51,8 @@ const importEnable = computed(() => {
const onConfirmImport = async () => {
try {
importing.value = true
const { server, db, file, conflict } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict).catch((e) => {})
const { server, db, file, conflict, expire } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict, expire).catch((e) => {})
} catch (e) {
$message.error(e.message)
return
@ -91,6 +93,11 @@ const onClose = () => {
:placeholder="$t('dialogue.import.open_csv_file_tip')"
ext="csv" />
</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-radio-group v-model:value="importKeyForm.conflict">
<n-radio-button
@ -110,7 +117,7 @@ const onClose = () => {
:disabled="!importEnable"
:focusable="false"
:loading="loading"
type="error"
type="primary"
@click="onConfirmImport">
{{ $t('dialogue.export.export') }}
</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">
<path
:stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24"
d="M6 24V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 23L24 32L15 23"
d="M33 15L24 6L15 15"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M23.9917 6V32"
d="M24 6V32"
stroke="currentColor"
stroke-linecap="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">
<path
:stroke-width="props.strokeWidth"
d="M6 24V42H42V24"
d="M6 24.0083V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 15L24 6L15 15"
d="M33 23L24 32L15 23"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M24 6V32"
d="M23.9917 6V32"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />

View File

@ -66,6 +66,7 @@ const dbSelectOptions = computed(() => {
const moreOptions = computed(() => {
return [
{ 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: 'disconnect',

View File

@ -282,6 +282,8 @@
},
"import": {
"name": "Import Data",
"export_expire_title": "Expiration",
"export_expire": "Export Expiration Time",
"import": "Import",
"open_csv_file": "Import File",
"open_csv_file_tip": "Select the file for import",
@ -296,6 +298,8 @@
},
"upgrade": {
"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?",
"no_update": "You're update to date",
"download_now": "Download",

View File

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

View File

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