diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 35f6fa4..5515bd3 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -2234,7 +2234,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, 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 conf := Connection().getConnection(server) if conf == nil { @@ -2270,14 +2270,14 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict var line []string var readErr error var key, value []byte - var ttl time.Duration + var ttlValue time.Duration var imported, ignored int64 var canceled bool startTime := time.Now().Add(-10 * time.Second) for { readErr = nil - ttl = redis.KeepTTL + ttlValue = redis.KeepTTL line, readErr = reader.Read() if readErr != nil { break @@ -2293,21 +2293,25 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict continue } // 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 { - 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 { - readErr = client.RestoreReplace(ctx, string(key), ttl, string(value)).Err() + readErr = client.RestoreReplace(ctx, string(key), ttlValue, 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, ttl, string(value)).Err() + readErr = client.Restore(ctx, keyStr, ttlValue, string(value)).Err() } else { - readErr = errors.New("key existed") + readErr = errors.New("key already existed") } } if readErr != nil { diff --git a/frontend/src/components/common/TtlInput.vue b/frontend/src/components/common/TtlInput.vue new file mode 100644 index 0000000..283e862 --- /dev/null +++ b/frontend/src/components/common/TtlInput.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/dialogs/ImportKeyDialog.vue b/frontend/src/components/dialogs/ImportKeyDialog.vue index 1387fd9..ab52f72 100644 --- a/frontend/src/components/dialogs/ImportKeyDialog.vue +++ b/frontend/src/components/dialogs/ImportKeyDialog.vue @@ -5,15 +5,18 @@ import { useI18n } from 'vue-i18n' import useBrowserStore from 'stores/browser.js' import { isEmpty } from 'lodash' import FileOpenInput from '@/components/common/FileOpenInput.vue' +import TtlInput from '@/components/common/TtlInput.vue' const importKeyForm = reactive({ server: '', db: 0, - expire: true, reload: true, file: '', type: 0, conflict: 0, + ttlType: 0, + ttl: -1, + ttlUnit: 1, }) const dialogStore = useDialog() @@ -25,11 +28,12 @@ watchEffect(() => { const { server, db } = dialogStore.importKeyParam importKeyForm.server = server importKeyForm.db = db - importKeyForm.expire = true importKeyForm.reload = true importKeyForm.file = '' importKeyForm.type = 0 importKeyForm.conflict = 0 + importKeyForm.ttlType = 0 + importKeyForm.ttl = -1 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(() => { return !isEmpty(importKeyForm.file) }) @@ -53,8 +72,19 @@ const importEnable = computed(() => { const onConfirmImport = async () => { try { importing.value = true - const { server, db, file, conflict, expire, reload } = importKeyForm - browserStore.importKeysFromCSVFile(server, db, file, conflict, expire, reload).catch((e) => {}) + const { server, db, file, conflict, ttlType, ttl, ttlUnit, reload } = importKeyForm + 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) { $message.error(e.message) return @@ -104,16 +134,22 @@ const onClose = () => { :value="op.value" /> - + - - {{ $t('dialogue.import.import_expire') }} - - - {{ $t('dialogue.import.reload') }} - + + + + + + + {{ $t('dialogue.import.reload') }} + + diff --git a/frontend/src/components/dialogs/SetTtlDialog.vue b/frontend/src/components/dialogs/SetTtlDialog.vue index af20cf0..b5be274 100644 --- a/frontend/src/components/dialogs/SetTtlDialog.vue +++ b/frontend/src/components/dialogs/SetTtlDialog.vue @@ -4,6 +4,7 @@ import useDialog from 'stores/dialog' import useBrowserStore from 'stores/browser.js' import { useI18n } from 'vue-i18n' import { isEmpty, size } from 'lodash' +import TtlInput from '@/components/common/TtlInput.vue' const ttlForm = reactive({ server: '', @@ -42,22 +43,6 @@ const isBatchAction = computed(() => { }) 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(() => [ { value: -1, unit: 1, label: i18n.t('interface.forever') }, { value: 10, unit: 1, label: `10 ${i18n.t('common.second')}` }, @@ -119,15 +104,7 @@ const onConfirm = async () => { - - - - + diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue index 046b00f..4c8821a 100644 --- a/frontend/src/components/sidebar/BrowserTree.vue +++ b/frontend/src/components/sidebar/BrowserTree.vue @@ -565,7 +565,7 @@ watchEffect( ) // 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) defineExpose({ handleSelectContextMenu, diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json index 5b5ae6a..2f1b3cf 100644 --- a/frontend/src/langs/en-us.json +++ b/frontend/src/langs/en-us.json @@ -291,7 +291,6 @@ "import": { "name": "Import Data", "import_expire_title": "Expiration", - "import_expire": "Import Expiration Time", "import": "Import", "reload": "Reload Keys After Imported", "open_csv_file": "Import File", @@ -299,6 +298,9 @@ "conflict_handle": "Key Conflict Resolution", "conflict_overwrite": "Overwrite", "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}", "import_completed": "Import completed, {success} successes, {ignored} failed" }, diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index e040aca..e10b2d3 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -291,7 +291,6 @@ "import": { "name": "导入数据", "import_expire_title": "过期时间", - "import_expire": "包含键过期时间", "reload": "导入完成后重新载入", "import": "确认导入", "open_csv_file": "导入文件路径", @@ -299,6 +298,9 @@ "conflict_handle": "键冲突处理", "conflict_overwrite": "覆盖", "conflict_ignore": "忽略", + "ttl_include": "尝试导入", + "ttl_ignore": "不设置", + "ttl_custom": "自定义", "importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}", "import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个" }, diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index a7d7031..61da84e 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -1738,11 +1738,11 @@ const useBrowserStore = defineStore('browser', { * @param {number} db * @param {string} path * @param {number} conflict - * @param {boolean} [expire] + * @param {number} [ttl] <0:use previous; ==0: persist; >0: custom ttl * @param {boolean} [reload] * @return {Promise} */ - async importKeysFromCSVFile(server, db, path, conflict, expire, reload) { + async importKeysFromCSVFile(server, db, path, conflict, ttl, reload) { const msgRef = $message.loading('', { duration: 0, closable: true }) let imported = 0 let ignored = 0 @@ -1760,7 +1760,7 @@ const useBrowserStore = defineStore('browser', { msgRef.onClose = () => { 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) { canceled = get(data, 'canceled', false) imported = get(data, 'imported', 0) diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js index 0da647d..19ad166 100644 --- a/frontend/src/stores/tab.js +++ b/frontend/src/stores/tab.js @@ -674,7 +674,7 @@ const useTabStore = defineStore('tab', { * @param {string} server * @param {CheckedKey[]} [keys] */ - setCheckedKeys(server, keys) { + setCheckedKeys(server, keys = null) { let tab = find(this.tabList, { name: server }) if (tab != null) { if (isEmpty(keys)) {