feat: add expire time data for import/export handle
This commit is contained in:
parent
3fe8767c44
commit
f597002378
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "选择需要导入的文件",
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue