mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: initial db export implementation
This commit is contained in:
@ -30,6 +30,7 @@ module.exports = {
|
|||||||
functionAdd: true,
|
functionAdd: true,
|
||||||
schedulerAdd: true,
|
schedulerAdd: true,
|
||||||
schemaEdit: true,
|
schemaEdit: true,
|
||||||
|
schemaExport: true,
|
||||||
tableSettings: true,
|
tableSettings: true,
|
||||||
viewSettings: true,
|
viewSettings: true,
|
||||||
triggerSettings: true,
|
triggerSettings: true,
|
||||||
|
@ -27,6 +27,7 @@ module.exports = {
|
|||||||
routineAdd: true,
|
routineAdd: true,
|
||||||
functionAdd: true,
|
functionAdd: true,
|
||||||
databaseEdit: false,
|
databaseEdit: false,
|
||||||
|
schemaExport: true,
|
||||||
tableSettings: true,
|
tableSettings: true,
|
||||||
viewSettings: true,
|
viewSettings: true,
|
||||||
triggerSettings: true,
|
triggerSettings: true,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain, dialog } from 'electron';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
ipcMain.on('close-app', () => {
|
ipcMain.on('close-app', () => {
|
||||||
@ -9,4 +9,12 @@ export default () => {
|
|||||||
const key = false;
|
const key = false;
|
||||||
event.returnValue = key;
|
event.returnValue = key;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('showOpenDialog', (event, options) => {
|
||||||
|
return dialog.showOpenDialog(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-download-dir-path', () => {
|
||||||
|
return app.getPath('downloads');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { ipcMain, dialog, Notification } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
import { ipcMain } from 'electron';
|
import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
|
||||||
|
|
||||||
export default connections => {
|
export default connections => {
|
||||||
|
let exporter = null;
|
||||||
|
|
||||||
ipcMain.handle('create-schema', async (event, params) => {
|
ipcMain.handle('create-schema', async (event, params) => {
|
||||||
try {
|
try {
|
||||||
await connections[params.uid].createSchema(params);
|
await connections[params.uid].createSchema(params);
|
||||||
@ -37,9 +42,16 @@ export default connections => {
|
|||||||
|
|
||||||
ipcMain.handle('get-schema-collation', async (event, params) => {
|
ipcMain.handle('get-schema-collation', async (event, params) => {
|
||||||
try {
|
try {
|
||||||
const collation = await connections[params.uid].getDatabaseCollation(params);
|
const collation = await connections[params.uid].getDatabaseCollation(
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
|
return {
|
||||||
|
status: 'success',
|
||||||
|
response: collation.rows.length
|
||||||
|
? collation.rows[0].DEFAULT_COLLATION_NAME
|
||||||
|
: ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
return { status: 'error', response: err.toString() };
|
return { status: 'error', response: err.toString() };
|
||||||
@ -48,7 +60,9 @@ export default connections => {
|
|||||||
|
|
||||||
ipcMain.handle('get-structure', async (event, params) => {
|
ipcMain.handle('get-structure', async (event, params) => {
|
||||||
try {
|
try {
|
||||||
const structure = await connections[params.uid].getStructure(params.schemas);
|
const structure = await connections[params.uid].getStructure(
|
||||||
|
params.schemas
|
||||||
|
);
|
||||||
|
|
||||||
return { status: 'success', response: structure };
|
return { status: 'success', response: structure };
|
||||||
}
|
}
|
||||||
@ -152,4 +166,98 @@ export default connections => {
|
|||||||
return { status: 'error', response: err.toString() };
|
return { status: 'error', response: err.toString() };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('export', async (event, { uid, ...rest }) => {
|
||||||
|
if (exporter !== null) return;
|
||||||
|
|
||||||
|
const type = connections[uid]._client;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'mysql':
|
||||||
|
exporter = new MysqlExporter(connections[uid], rest);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
response: `${type} exporter not aviable`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFileName = path.basename(rest.outputFile);
|
||||||
|
|
||||||
|
if (fs.existsSync(rest.outputFile)) {
|
||||||
|
const result = await dialog.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: `File ${outputFileName} already exists. Do you want to replace it?`,
|
||||||
|
detail:
|
||||||
|
'A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.',
|
||||||
|
buttons: ['Cancel', 'Replace'],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response !== 1) {
|
||||||
|
exporter = null;
|
||||||
|
return { status: 'error', response: 'Operation aborted' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
exporter.once('error', err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
exporter.once('end', () => {
|
||||||
|
resolve({ cancelled: exporter.isCancelled });
|
||||||
|
});
|
||||||
|
|
||||||
|
exporter.on('progress', state => {
|
||||||
|
event.sender.send('export-progress', state);
|
||||||
|
});
|
||||||
|
|
||||||
|
exporter.run();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.cancelled) {
|
||||||
|
new Notification({
|
||||||
|
title: 'Export finished',
|
||||||
|
body: `Finished exporting to ${outputFileName}`
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
return { status: 'success', response };
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
new Notification({
|
||||||
|
title: 'Export error',
|
||||||
|
body: err.toString()
|
||||||
|
}).show();
|
||||||
|
|
||||||
|
return { status: 'error', response: err.toString() };
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
exporter.removeAllListeners();
|
||||||
|
exporter = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('abort-export', async event => {
|
||||||
|
let willAbort = false;
|
||||||
|
|
||||||
|
if (exporter) {
|
||||||
|
const result = await dialog.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Are you sure you want to abort the export',
|
||||||
|
buttons: ['Cancel', 'Abort'],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response === 1) {
|
||||||
|
willAbort = true;
|
||||||
|
exporter.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'success', response: { willAbort } };
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
73
src/main/libs/exporters/BaseExporter.js
Normal file
73
src/main/libs/exporters/BaseExporter.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
export class BaseExporter extends EventEmitter {
|
||||||
|
constructor (options) {
|
||||||
|
super();
|
||||||
|
this._options = options;
|
||||||
|
this._isCancelled = false;
|
||||||
|
this._outputStream = fs.createWriteStream(this._options.outputFile, {
|
||||||
|
flags: 'w'
|
||||||
|
});
|
||||||
|
this._state = {};
|
||||||
|
|
||||||
|
this._outputStream.once('error', err => {
|
||||||
|
this._isCancelled = true;
|
||||||
|
this.emit('error', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async run () {
|
||||||
|
try {
|
||||||
|
this.emit('start', this);
|
||||||
|
await this.dump();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.emit('error', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this._outputStream.end();
|
||||||
|
this.emit('end');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCancelled () {
|
||||||
|
return this._isCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFileExists () {
|
||||||
|
return fs.existsSync(this._options.outputFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this._isCancelled = true;
|
||||||
|
this.emit('cancel');
|
||||||
|
this.emitUpdate({ op: 'cancelling' });
|
||||||
|
}
|
||||||
|
|
||||||
|
emitUpdate (state) {
|
||||||
|
this.emit('progress', { ...this._state, ...state });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeString (data) {
|
||||||
|
if (this._isCancelled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.accessSync(this._options.outputFile);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this._isCancelled = true;
|
||||||
|
|
||||||
|
const fileName = path.basename(this._options.outputFile);
|
||||||
|
this.emit('error', `The file ${fileName} is not accessible`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._outputStream.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
dump () {
|
||||||
|
throw new Error('Exporter must implement the "dump" method');
|
||||||
|
}
|
||||||
|
}
|
35
src/main/libs/exporters/ExporterFactory.js
Normal file
35
src/main/libs/exporters/ExporterFactory.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { MysqlExporter } from './sql/MysqlExporter';
|
||||||
|
|
||||||
|
export class ExporterFactory {
|
||||||
|
/**
|
||||||
|
* Returns a data exporter class instance.
|
||||||
|
*
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {String} args.client
|
||||||
|
* @param {Object} args.params
|
||||||
|
* @param {String} args.params.host
|
||||||
|
* @param {Number} args.params.port
|
||||||
|
* @param {String} args.params.password
|
||||||
|
* @param {String=} args.params.database
|
||||||
|
* @param {String=} args.params.schema
|
||||||
|
* @param {String} args.params.ssh.host
|
||||||
|
* @param {String} args.params.ssh.username
|
||||||
|
* @param {String} args.params.ssh.password
|
||||||
|
* @param {Number} args.params.ssh.port
|
||||||
|
* @param {Number=} args.poolSize
|
||||||
|
* @returns Exporter Instance
|
||||||
|
* @memberof ExporterFactory
|
||||||
|
*/
|
||||||
|
static get (args) {
|
||||||
|
switch (type) {
|
||||||
|
case 'mysql':
|
||||||
|
exporter = new MysqlExporter(connections[uid], rest);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
response: `${type} exporter not aviable`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
src/main/libs/exporters/sql/MysqlExporter.js
Normal file
108
src/main/libs/exporters/sql/MysqlExporter.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { SqlExporter } from './SqlExporter';
|
||||||
|
import { BLOB, BIT } from 'common/fieldTypes';
|
||||||
|
import hexToBinary from 'common/libs/hexToBinary';
|
||||||
|
|
||||||
|
export default class MysqlExporter extends SqlExporter {
|
||||||
|
async getSqlHeader () {
|
||||||
|
let dump = await super.getSqlHeader();
|
||||||
|
dump += `
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
|
||||||
|
|
||||||
|
return dump;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFooter () {
|
||||||
|
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCreateTable (tableName) {
|
||||||
|
const { rows } = await this._client.raw(`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``);
|
||||||
|
|
||||||
|
if (rows.length !== 1)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
return rows[0]['Create Table'] + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDropTable (tableName) {
|
||||||
|
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableInsert (tableName) {
|
||||||
|
let rowCount = 0;
|
||||||
|
let sqlStr = '';
|
||||||
|
|
||||||
|
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``);
|
||||||
|
if (countResults.rows.length === 1)
|
||||||
|
rowCount = countResults.rows[0].count;
|
||||||
|
|
||||||
|
if (rowCount > 0) {
|
||||||
|
const columns = await this._client.getTableColumns({ table: tableName, schema: this.schemaName });
|
||||||
|
const columnNames = columns.map(col => '`' + col.name + '`');
|
||||||
|
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(', ')}) VALUES`;
|
||||||
|
|
||||||
|
const tableResult = await this._client.raw(`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``);
|
||||||
|
|
||||||
|
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
|
||||||
|
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
|
||||||
|
sqlStr += '\n\n';
|
||||||
|
|
||||||
|
sqlStr += insertStmt;
|
||||||
|
sqlStr += '\n';
|
||||||
|
|
||||||
|
for (const row of tableResult.rows) {
|
||||||
|
sqlStr += '\t(';
|
||||||
|
|
||||||
|
for (const i in columns) {
|
||||||
|
const column = columns[i];
|
||||||
|
const val = row[column.name];
|
||||||
|
|
||||||
|
if (val === null)
|
||||||
|
sqlStr += 'NULL';
|
||||||
|
|
||||||
|
else if (BIT.includes(column.type))
|
||||||
|
sqlStr += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
|
||||||
|
|
||||||
|
else if (BLOB.includes(column.type))
|
||||||
|
sqlStr += `X'${val.toString('hex').toUpperCase()}'`;
|
||||||
|
|
||||||
|
else if (val === '')
|
||||||
|
sqlStr += '\'\'';
|
||||||
|
|
||||||
|
else
|
||||||
|
sqlStr += typeof val === 'string' ? this.escapeAndQuote(val) : val;
|
||||||
|
|
||||||
|
if (parseInt(i) !== columns.length - 1)
|
||||||
|
sqlStr += ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlStr += '),\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlStr += '\n';
|
||||||
|
|
||||||
|
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
|
||||||
|
sqlStr += 'UNLOCK TABLES;';
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeAndQuote (value) {
|
||||||
|
if (!value) return null;
|
||||||
|
return `'${value.replaceAll(/'/g, '\'\'')}'`;
|
||||||
|
}
|
||||||
|
}
|
122
src/main/libs/exporters/sql/SqlExporter.js
Normal file
122
src/main/libs/exporters/sql/SqlExporter.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { BaseExporter } from '../BaseExporter';
|
||||||
|
|
||||||
|
export class SqlExporter extends BaseExporter {
|
||||||
|
constructor (client, options) {
|
||||||
|
super(options);
|
||||||
|
this._client = client;
|
||||||
|
this._commentChar = '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
get schemaName () {
|
||||||
|
return this._options.schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
get host () {
|
||||||
|
return this._client._params.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServerVersion () {
|
||||||
|
const version = await this._client.getVersion();
|
||||||
|
return `${version.name} ${version.number}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async dump () {
|
||||||
|
const exportState = {
|
||||||
|
totalItems: this._options.items.length,
|
||||||
|
currentItemIndex: 0,
|
||||||
|
currentItem: '',
|
||||||
|
op: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = await this.getSqlHeader();
|
||||||
|
this.writeString(header);
|
||||||
|
this.writeString('\n\n\n');
|
||||||
|
|
||||||
|
for (const item of this._options.items) {
|
||||||
|
// user abort operation
|
||||||
|
if (this.isCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// skip item if not set to output any detail for them
|
||||||
|
if (!item.includeStructure && !item.includeContent && !item.includeDropStatement)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
exportState.currentItemIndex++;
|
||||||
|
exportState.currentItem = item.table;
|
||||||
|
exportState.op = 'PROCESSING';
|
||||||
|
|
||||||
|
this.emitUpdate(exportState);
|
||||||
|
|
||||||
|
const tableHeader = this.buildComment(`Dump of table ${item.table}\n------------------------------------------------------------`);
|
||||||
|
this.writeString(tableHeader);
|
||||||
|
this.writeString('\n\n');
|
||||||
|
|
||||||
|
if (item.includeDropStatement) {
|
||||||
|
const dropTableSyntax = this.getDropTable(item.table);
|
||||||
|
this.writeString(dropTableSyntax);
|
||||||
|
this.writeString('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.includeStructure) {
|
||||||
|
const createTableSyntax = await this.getCreateTable(item.table);
|
||||||
|
this.writeString(createTableSyntax);
|
||||||
|
this.writeString('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.includeContent) {
|
||||||
|
exportState.op = 'FETCH';
|
||||||
|
this.emitUpdate(exportState);
|
||||||
|
const tableInsertSyntax = await this.getTableInsert(item.table);
|
||||||
|
|
||||||
|
exportState.op = 'WRITE';
|
||||||
|
this.emitUpdate(exportState);
|
||||||
|
this.writeString(tableInsertSyntax);
|
||||||
|
this.writeString('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writeString('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const footer = await this.getFooter();
|
||||||
|
this.writeString(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildComment (text) {
|
||||||
|
return text.split('\n').map(txt => `${this._commentChar} ${txt}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSqlHeader () {
|
||||||
|
const serverVersion = await this.getServerVersion();
|
||||||
|
const header = `************************************************************
|
||||||
|
Antares - SQL Client
|
||||||
|
Version ${app.getVersion()}
|
||||||
|
|
||||||
|
https://antares-sql.app/
|
||||||
|
https://github.com/Fabio286/antares
|
||||||
|
|
||||||
|
Host: ${this.host} (${serverVersion})
|
||||||
|
Database: ${this.schemaName}
|
||||||
|
Generation time: ${moment().format()}
|
||||||
|
************************************************************`;
|
||||||
|
|
||||||
|
return this.buildComment(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFooter () {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTable (tableName) {
|
||||||
|
throw new Error('Sql Exporter must implement the "getCreateTable" method');
|
||||||
|
}
|
||||||
|
|
||||||
|
getDropTable (tableName) {
|
||||||
|
throw new Error('Sql Exporter must implement the "getDropTable" method');
|
||||||
|
}
|
||||||
|
|
||||||
|
getTableInsert (tableName) {
|
||||||
|
throw new Error('Sql Exporter must implement the "getTableInsert" method');
|
||||||
|
}
|
||||||
|
}
|
19
src/main/workers/ExportService.js
Normal file
19
src/main/workers/ExportService.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Worker, isMainThread, workerData, parentPort } from 'worker_threads';
|
||||||
|
import
|
||||||
|
|
||||||
|
if (isMainThread) {
|
||||||
|
module.exports = function run (workerData) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const worker = new Worker(__filename, { workerData });
|
||||||
|
worker.on('message', resolve);
|
||||||
|
worker.on('error', reject);
|
||||||
|
worker.on('exit', (code) => {
|
||||||
|
if (code !== 0)
|
||||||
|
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
}
|
304
src/renderer/components/ModalExportSchema.vue
Normal file
304
src/renderer/components/ModalExportSchema.vue
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal active">
|
||||||
|
<a class="modal-overlay" @click.stop="closeModal" />
|
||||||
|
<div class="modal-container p-0">
|
||||||
|
<div class="modal-header pl-2">
|
||||||
|
<div class="modal-title h6">
|
||||||
|
<div class="d-flex">
|
||||||
|
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
|
||||||
|
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body pb-0">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="col-3">
|
||||||
|
<label class="form-label">{{ $t('message.directoryPath') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<fieldset class="input-group">
|
||||||
|
<input
|
||||||
|
v-model="basePath"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
readonly
|
||||||
|
:placeholder="$t('message.schemaName')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary input-group-btn"
|
||||||
|
@click.prevent="openPathDialog"
|
||||||
|
>
|
||||||
|
{{ $t('word.change') }}
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns mb-2">
|
||||||
|
<div class="column col-auto d-flex p-0 text-italic ">
|
||||||
|
<i class="mdi mdi-file-document-outline mr-2" />
|
||||||
|
{{ filename }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column col-auto col-ml-auto p-0">
|
||||||
|
<button class="btn btn-dark btn-sm" @click="uncheckAllTables">
|
||||||
|
<i class="mdi mdi-file-tree-outline" />
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-dark btn-sm" @click="checkAllTables">
|
||||||
|
<i class="mdi mdi-file-tree" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workspace-query-results">
|
||||||
|
<div ref="table" class="table table-hover">
|
||||||
|
<div class="thead">
|
||||||
|
<div class="tr">
|
||||||
|
<div class="th c-hand" style="width: 50%;">
|
||||||
|
<div class="table-column-title">
|
||||||
|
<span>Table</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="th c-hand">
|
||||||
|
<div class="table-column-title">
|
||||||
|
<span>Structure</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="th c-hand">
|
||||||
|
<div class="table-column-title">
|
||||||
|
<span>Content</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="th c-hand">
|
||||||
|
<div class="table-column-title">
|
||||||
|
<span>Drop</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbody">
|
||||||
|
<div
|
||||||
|
v-for="item in tables"
|
||||||
|
:key="item.name"
|
||||||
|
class="tr"
|
||||||
|
>
|
||||||
|
<div class="td">
|
||||||
|
{{ item.table }}
|
||||||
|
</div>
|
||||||
|
<div class="td">
|
||||||
|
<label class="form-checkbox m-0 px-2">
|
||||||
|
<input
|
||||||
|
v-model="item.includeStructure"
|
||||||
|
type="checkbox"
|
||||||
|
><i class="form-icon" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="td">
|
||||||
|
<label class="form-checkbox m-0 px-2">
|
||||||
|
<input
|
||||||
|
v-model="item.includeContent"
|
||||||
|
type="checkbox"
|
||||||
|
><i class="form-icon" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="td">
|
||||||
|
<label class="form-checkbox m-0 px-2">
|
||||||
|
<input
|
||||||
|
v-model="item.includeDropStatement"
|
||||||
|
type="checkbox"
|
||||||
|
><i class="form-icon" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="progressPercentage > 0">
|
||||||
|
<progress
|
||||||
|
class="progress"
|
||||||
|
:value="progressPercentage"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
<p class="empty-subtitle">
|
||||||
|
{{ progressPercentage }}% - {{ progressStatus }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary mr-2"
|
||||||
|
:class="{'loading': isExporting}"
|
||||||
|
:disabled="isExporting"
|
||||||
|
@click.stop="startExport"
|
||||||
|
>
|
||||||
|
{{ $t('word.export') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-link" @click.stop="closeModal">
|
||||||
|
{{ $t('word.close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Application from '@/ipc-api/Application';
|
||||||
|
import Schema from '@/ipc-api/Schema';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ModalExportSchema',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
selectedSchema: String
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isExporting: false,
|
||||||
|
progressPercentage: 0,
|
||||||
|
progressStatus: '',
|
||||||
|
tables: [],
|
||||||
|
basePath: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
|
getWorkspace: 'workspaces/getWorkspace',
|
||||||
|
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||||
|
}),
|
||||||
|
currentWorkspace () {
|
||||||
|
return this.getWorkspace(this.selectedWorkspace);
|
||||||
|
},
|
||||||
|
schemaItems () {
|
||||||
|
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
|
||||||
|
if (db)
|
||||||
|
return db.tables.filter(table => table.type === 'table');
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
filename () {
|
||||||
|
const date = moment().format('YYYY-MM-DD');
|
||||||
|
return `${this.selectedSchema}_${date}.sql`;
|
||||||
|
},
|
||||||
|
dumpFilePath () {
|
||||||
|
return `${this.basePath}/${this.filename}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
this.basePath = await Application.getDownloadPathDirectory();
|
||||||
|
this.tables = this.schemaItems.map(item => ({
|
||||||
|
table: item.name,
|
||||||
|
includeStructure: true,
|
||||||
|
includeContent: true,
|
||||||
|
includeDropStatement: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
ipcRenderer.on('export-progress', this.updateProgress);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
ipcRenderer.off('export-progress', this.updateProgress);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
refreshSchema: 'workspaces/refreshSchema'
|
||||||
|
}),
|
||||||
|
async startExport () {
|
||||||
|
this.isExporting = true;
|
||||||
|
const { uid } = this.currentWorkspace;
|
||||||
|
const params = {
|
||||||
|
uid,
|
||||||
|
schema: this.selectedSchema,
|
||||||
|
outputFile: this.dumpFilePath,
|
||||||
|
items: [...this.tables]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await Schema.export(params);
|
||||||
|
if (result) {
|
||||||
|
if (result.status === 'success')
|
||||||
|
this.progressStatus = result.response.cancelled ? 'Aborted' : 'Completed!';
|
||||||
|
|
||||||
|
else
|
||||||
|
this.progressStatus = result.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isExporting = false;
|
||||||
|
},
|
||||||
|
updateProgress (event, state) {
|
||||||
|
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
|
||||||
|
this.progressStatus = state.op + ' ' + state.currentItem;
|
||||||
|
},
|
||||||
|
async closeModal () {
|
||||||
|
let willClose = true;
|
||||||
|
if (this.isExporting) {
|
||||||
|
willClose = false;
|
||||||
|
const { response } = await Schema.abortExport();
|
||||||
|
willClose = response.willAbort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (willClose)
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Escape')
|
||||||
|
this.closeModal();
|
||||||
|
},
|
||||||
|
checkAllTables () {
|
||||||
|
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: true, includeContent: true, includeDropStatement: true }));
|
||||||
|
},
|
||||||
|
uncheckAllTables () {
|
||||||
|
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: false, includeContent: false, includeDropStatement: false }));
|
||||||
|
},
|
||||||
|
async openPathDialog () {
|
||||||
|
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
|
||||||
|
if (result && !result.canceled)
|
||||||
|
this.basePath = result.filePaths[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.workspace-query-results {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
min-height: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.form-icon {
|
||||||
|
top: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -58,6 +58,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="workspace.customizations.schemaExport"
|
||||||
|
class="context-element"
|
||||||
|
@click="showExportSchemaModal"
|
||||||
|
>
|
||||||
|
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="workspace.customizations.schemaEdit"
|
v-if="workspace.customizations.schemaEdit"
|
||||||
class="context-element"
|
class="context-element"
|
||||||
@ -91,6 +98,11 @@
|
|||||||
:selected-schema="selectedSchema"
|
:selected-schema="selectedSchema"
|
||||||
@close="hideEditModal"
|
@close="hideEditModal"
|
||||||
/>
|
/>
|
||||||
|
<ModalExportSchema
|
||||||
|
v-if="isExportSchemaModal"
|
||||||
|
:selected-schema="selectedSchema"
|
||||||
|
@close="hideExportSchemaModal"
|
||||||
|
/>
|
||||||
</BaseContextMenu>
|
</BaseContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -99,6 +111,7 @@ import { mapGetters, mapActions } from 'vuex';
|
|||||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||||
import ModalEditSchema from '@/components/ModalEditSchema';
|
import ModalEditSchema from '@/components/ModalEditSchema';
|
||||||
|
import ModalExportSchema from '@/components/ModalExportSchema';
|
||||||
import Schema from '@/ipc-api/Schema';
|
import Schema from '@/ipc-api/Schema';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -106,7 +119,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
BaseContextMenu,
|
BaseContextMenu,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
ModalEditSchema
|
ModalEditSchema,
|
||||||
|
ModalExportSchema
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
contextEvent: MouseEvent,
|
contextEvent: MouseEvent,
|
||||||
@ -115,7 +129,8 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isDeleteModal: false,
|
isDeleteModal: false,
|
||||||
isEditModal: false
|
isEditModal: false,
|
||||||
|
isExportSchemaModal: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -166,6 +181,12 @@ export default {
|
|||||||
this.isEditModal = false;
|
this.isEditModal = false;
|
||||||
this.closeContext();
|
this.closeContext();
|
||||||
},
|
},
|
||||||
|
showExportSchemaModal () {
|
||||||
|
this.isExportSchemaModal = true;
|
||||||
|
},
|
||||||
|
hideExportSchemaModal () {
|
||||||
|
this.isExportSchemaModal = false;
|
||||||
|
},
|
||||||
closeContext () {
|
closeContext () {
|
||||||
this.$emit('close-context');
|
this.$emit('close-context');
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,8 @@ module.exports = {
|
|||||||
history: 'History',
|
history: 'History',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
passphrase: 'Passphrase',
|
passphrase: 'Passphrase',
|
||||||
filter: 'Filter'
|
filter: 'Filter',
|
||||||
|
change: 'Change'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
appWelcome: 'Welcome to Antares SQL Client!',
|
appWelcome: 'Welcome to Antares SQL Client!',
|
||||||
@ -246,7 +247,9 @@ module.exports = {
|
|||||||
thereIsNoQueriesYet: 'There is no queries yet',
|
thereIsNoQueriesYet: 'There is no queries yet',
|
||||||
searchForQueries: 'Search for queries',
|
searchForQueries: 'Search for queries',
|
||||||
killProcess: 'Kill process',
|
killProcess: 'Kill process',
|
||||||
closeTab: 'Close tab'
|
closeTab: 'Close tab',
|
||||||
|
exportSchema: 'Export schema',
|
||||||
|
directoryPath: 'Directory path'
|
||||||
},
|
},
|
||||||
faker: {
|
faker: {
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
@ -121,7 +121,8 @@ module.exports = {
|
|||||||
history: 'Cronologia',
|
history: 'Cronologia',
|
||||||
select: 'Seleziona',
|
select: 'Seleziona',
|
||||||
passphrase: 'Passphrase',
|
passphrase: 'Passphrase',
|
||||||
filter: 'Filtra'
|
filter: 'Filtra',
|
||||||
|
change: 'Cambia'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
appWelcome: 'Benvenuto in Antares SQL Client!',
|
appWelcome: 'Benvenuto in Antares SQL Client!',
|
||||||
@ -233,7 +234,9 @@ module.exports = {
|
|||||||
duplicateTable: 'Duplica tabella',
|
duplicateTable: 'Duplica tabella',
|
||||||
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
|
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
|
||||||
noSchema: 'Nessuno schema',
|
noSchema: 'Nessuno schema',
|
||||||
restorePreviourSession: 'Ripristina sessione precedente'
|
restorePreviourSession: 'Ripristina sessione precedente',
|
||||||
|
exportSchema: 'Esporta schema',
|
||||||
|
directoryPath: 'Percorso directory'
|
||||||
},
|
},
|
||||||
faker: {
|
faker: {
|
||||||
address: 'Indirizzo',
|
address: 'Indirizzo',
|
||||||
|
@ -5,4 +5,12 @@ export default class {
|
|||||||
static getKey (params) {
|
static getKey (params) {
|
||||||
return ipcRenderer.sendSync('get-key', params);
|
return ipcRenderer.sendSync('get-key', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static showOpenDialog (options) {
|
||||||
|
return ipcRenderer.invoke('showOpenDialog', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDownloadPathDirectory () {
|
||||||
|
return ipcRenderer.invoke('get-download-dir-path');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,4 +53,12 @@ export default class {
|
|||||||
static rawQuery (params) {
|
static rawQuery (params) {
|
||||||
return ipcRenderer.invoke('raw-query', params);
|
return ipcRenderer.invoke('raw-query', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static export (params) {
|
||||||
|
return ipcRenderer.invoke('export', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static abortExport () {
|
||||||
|
return ipcRenderer.invoke('abort-export');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user