Skip to content

Commit

Permalink
feat: implement backup index to json file (#136)
Browse files Browse the repository at this point in the history
feat: implement backup index to json file

- [x] export index to json file
- [x] success message
- [x] failure message, delete uncompleted file if the task failed in the
middle of progress
- [x] progress bar or notification keeps the progress visible to the
user
- [x] overwrite warning if the target file already exists


Refs: #23

---------

Signed-off-by: seven <[email protected]>
  • Loading branch information
Blankll authored Nov 17, 2024
1 parent e330018 commit 4f1a76d
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 118 deletions.
3 changes: 2 additions & 1 deletion src/datasources/fetchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ const loadHttpClient = (con: {
password?: string;
sslCertVerification: boolean;
}) => ({
get: async (path?: string, queryParameters?: string) =>
get: async (path?: string, queryParameters?: string, payload?: string) =>
fetchWrapper({
...con,
method: 'GET',
path,
queryParameters,
payload,
ssl: con.sslCertVerification,
}),
post: async (path: string, queryParameters?: string, payload?: string) =>
Expand Down
8 changes: 5 additions & 3 deletions src/datasources/sourceFileApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {

import { CustomError, debug } from '../common';

const saveFile = async (filePath: string, content: string) => {
const saveFile = async (filePath: string, content: string, append: boolean) => {
try {
const folderPath = filePath.substring(0, filePath.lastIndexOf('/'));

if (!(await exists(folderPath, { dir: BaseDirectory.AppData }))) {
await createDir(folderPath, { dir: BaseDirectory.AppData, recursive: true });
}
await writeTextFile(filePath, content, { dir: BaseDirectory.AppConfig, append: false });
await writeTextFile(filePath, content, { dir: BaseDirectory.AppConfig, append });
debug('save file success');
} catch (err) {
debug(`saveFile error: ${err}`);
Expand Down Expand Up @@ -60,11 +60,13 @@ const renameFileOrFolder = async (oldPath: string, newPath: string) => {
};

const sourceFileApi = {
saveFile: (filePath: string, content: string) => saveFile(filePath, content),
saveFile: (filePath: string, content: string, append = false) =>
saveFile(filePath, content, append),
readFile: (filePath: string) => readFromFile(filePath),
createFolder: (folderPath: string) => createDir(folderPath),
deleteFileOrFolder,
renameFileOrFolder,
exists: (filePath: string) => exists(filePath),
};

export { sourceFileApi };
3 changes: 3 additions & 0 deletions src/lang/enUS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const enUS = {
closeSuccess: 'Closed successfully',
openSuccess: 'Opened successfully',
switchSuccess: 'Switched successfully',
overwriteFile: 'File already exists, do you want to overwrite it?',
},
editor: {
establishedRequired: 'Select a DB instance before execute actions',
Expand Down Expand Up @@ -175,6 +176,7 @@ export const enUS = {
backup: 'Backup',
restore: 'Restore',
restoreSourceDesc: 'Click or drag a file to this area to upload your JSON/CSV file',
backupToFileSuccess: 'Successfully backed up to file',
backupForm: {
connection: 'Connection',
index: 'Index',
Expand All @@ -186,6 +188,7 @@ export const enUS = {
backupFileNameRequired: 'Please enter Backup File Name',
validationFailed: 'Backup Config validation failed!',
validationPassed: 'Backup Config validation passed!',
backupFileTypeInvalid: 'Backup file type is invalid',
},
},
version: {
Expand Down
3 changes: 3 additions & 0 deletions src/lang/zhCN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const zhCN = {
closeSuccess: '关闭成功',
openSuccess: '开启成功',
switchSuccess: '切换成功',
overwriteFile: '文件已存在,确定覆盖?',
},
editor: {
establishedRequired: '请选择执行操作的数据库实例',
Expand Down Expand Up @@ -176,6 +177,7 @@ export const zhCN = {
backup: '备份',
restore: '恢复',
restoreSourceDesc: '点击或拖动文件到此区域上传您的 JSON/CSV 文件',
backupToFileSuccess: '成功备份到文件',
backupForm: {
connection: '选择连接',
index: '选择索引',
Expand All @@ -187,6 +189,7 @@ export const zhCN = {
backupFileNameRequired: '请输入备份文件名',
validationFailed: '备份操作配置校验失败!',
validationPassed: '备份操作配置校验通过!',
backupFileTypeInvalid: '备份文件类型无效',
},
},
version: {
Expand Down
109 changes: 108 additions & 1 deletion src/store/backupRestoreStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@ import { open } from '@tauri-apps/api/dialog';
import { defineStore } from 'pinia';
import { CustomError } from '../common';
import { get } from 'lodash';
import { Connection } from './connectionStore.ts';
import { loadHttpClient, sourceFileApi } from '../datasources';

export type typeBackupInput = {
connection: Connection;
index: string;
backupFolder: string;
backupFileName: string;
backupFileType: string;
};

export const useBackupRestoreStore = defineStore('backupRestoreStore', {
state(): { folderPath: string; fileName: string } {
state(): {
folderPath: string;
fileName: string;
backupProgress: { complete: number; total: number } | null;
} {
return {
folderPath: '',
fileName: '',
backupProgress: null,
};
},
persist: true,
Expand All @@ -24,5 +39,97 @@ export const useBackupRestoreStore = defineStore('backupRestoreStore', {
);
}
},
async checkFileExist(input: Omit<typeBackupInput, 'connection'>) {
const filePath = `/${input.backupFolder}/${input.backupFileName}.${input.backupFileType}`;
try {
return await sourceFileApi.exists(filePath);
} catch (error) {
throw new CustomError(
get(error, 'status', 500),
get(error, 'details', get(error, 'message', '')),
);
}
},
async backupToFile(input: typeBackupInput) {
const client = loadHttpClient(input.connection);
const filePath = `${input.backupFolder}/${input.backupFileName}.${input.backupFileType}`;
let searchAfter: any[] | undefined = undefined;
let hasMore = true;

try {
this.backupProgress = {
complete: 0,
total: (await client.get(`/${input.index}/_count`)).count,
};

while (hasMore) {
const response = await client.get(
`/${input.index}/_search`,
undefined,
JSON.stringify({
size: 1000,
search_after: searchAfter,
sort: [{ _doc: 'asc' }],
}),
);
if (response.status && response.status !== 200) {
throw new CustomError(
response.status,
get(
response,
'details',
get(response, 'message', JSON.stringify(get(response, 'error.root_cause', ''))),
),
);
}

const hits = response.hits.hits;

this.backupProgress.complete += hits.length;
if (hits.length === 0) {
hasMore = false;
} else {
searchAfter = hits[hits.length - 1].sort;
const dataToWrite =
input.backupFileType === 'json'
? JSON.stringify(hits)
: JSON.stringify(convertToCsv(hits));
await sourceFileApi.saveFile(filePath, dataToWrite, true);
}
}
return filePath;
} catch (error) {
throw new CustomError(
get(error, 'status', 500),
get(error, 'details', get(error, 'message', '')),
);
}
},
},
});

const flattenObject = (obj: any, parent: string = '', res: any = {}) => {
for (let key in obj) {
const propName = parent ? `${parent}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
flattenObject(obj[key], propName, res);
} else {
res[propName] = obj[key];
}
}
return res;
};

const convertToCsv = (data: any[]) => {
if (data.length === 0) {
return { headers: [], data: [] };
}

const flattenedData = data.map(row => flattenObject(row));
const headers = Array.from(new Set(flattenedData.flatMap(row => Object.keys(row))));
const csvRows = flattenedData.map(row =>
headers.map(header => JSON.stringify(row[header] ?? '')).join(','),
);

return { headers, data: csvRows };
};
15 changes: 14 additions & 1 deletion src/store/connectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export const useConnectionStore = defineStore('connectionStore', {
establishedIndexNames(state) {
return state.established?.indices.map(({ index }) => index) ?? [];
},
establishedIndexOptions(state) {
return state.established?.indices.map(({ index }) => ({ label: index, value: index })) ?? [];
},
connectionOptions(state) {
return state.connections.map(({ name }) => ({ label: name, value: name }));
},
},
actions: {
async fetchConnections() {
Expand All @@ -93,8 +99,15 @@ export const useConnectionStore = defineStore('connectionStore', {
await storeApi.set('connections', pureObject(this.connections));
},
async establishConnection(connection: Connection) {
await this.testConnection(connection);
try {
await this.testConnection(connection);
} catch (err) {
this.established = null;
throw err;
}

const client = loadHttpClient(connection);

try {
const data = (await client.get('/_cat/indices', 'format=json')) as Array<{
[key: string]: string;
Expand Down
Loading

0 comments on commit 4f1a76d

Please sign in to comment.